gemini-computer-use 0.4.2 → 0.4.4
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 +18 -1086
- package/cli.js +56 -0
- package/package.json +14 -24
- package/LICENSE +0 -21
- package/bin/cli.js +0 -122
- package/core/__init__.py +0 -0
- package/core/agent.py +0 -767
- package/core/agent_harness.py +0 -67
- package/core/code_cache.py +0 -244
- package/core/device.py +0 -2400
- package/core/domain.py +0 -303
- package/core/hooks.py +0 -61
- package/core/mcp_tools.py +0 -3497
- package/core/policies.py +0 -132
- package/core/schemas.py +0 -49
- package/core/server.py +0 -972
- package/core/subagents.py +0 -55
- package/core/termux.py +0 -916
- package/core/triggers.py +0 -70
- package/core/updater.py +0 -1713
- package/core/version.py +0 -31
- package/core/vitals.py +0 -181
- package/core/web_share.py +0 -814
- package/gateway.py +0 -2862
- package/install-gui.cmd +0 -40
- package/install-gui.ps1 +0 -1882
- package/install-gui.strings.json +0 -211
- package/install.ps1 +0 -1287
- package/install.sh +0 -1998
- package/ops/doctor.ps1 +0 -735
- package/ops/mesh-domain.sh +0 -86
- package/ops/nginx/README.md +0 -77
- package/ops/nginx/antigravity-mesh-mcp.conf +0 -172
- package/ops/nginx/mesh-domain.vhost.template +0 -58
- package/ops/nginx/render-domain.sh +0 -123
- package/ops/update.ps1 +0 -373
- package/ops/windows/README.md +0 -298
- package/ops/windows/agent-watchdog.ps1 +0 -575
- package/ops/windows/run-hidden.vbs +0 -64
- package/skills/orchestrator.md +0 -26
package/install.sh
DELETED
|
@@ -1,1998 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ==============================================================================
|
|
3
|
-
# Antigravity Mesh - Universal Turnkey Installer (v0.1.2)
|
|
4
|
-
# Bilingual: English (Default) & Russian, Device Detection, SSH & Autostart
|
|
5
|
-
# Targets: Linux, macOS, Windows (Git Bash) and Android/Termux (phone/tablet)
|
|
6
|
-
# ==============================================================================
|
|
7
|
-
set -e
|
|
8
|
-
|
|
9
|
-
# Defaults
|
|
10
|
-
MODE=""
|
|
11
|
-
TLS="none"
|
|
12
|
-
PORT="8096"
|
|
13
|
-
DOMAIN=""
|
|
14
|
-
GATEWAY=""
|
|
15
|
-
USERNAME=""
|
|
16
|
-
TOKEN=""
|
|
17
|
-
DRY_RUN=false
|
|
18
|
-
QUICK=false
|
|
19
|
-
SSH_TARGET=""
|
|
20
|
-
SSH_PORT="22"
|
|
21
|
-
LANG_CHOICE=""
|
|
22
|
-
EXPLICIT_LANG=""
|
|
23
|
-
|
|
24
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
25
|
-
[ -z "$SCRIPT_DIR" ] && SCRIPT_DIR="."
|
|
26
|
-
|
|
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
|
|
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
|
-
}
|
|
288
|
-
|
|
289
|
-
CONFIG_DIR="$HOME/.config/antigravity-mesh"
|
|
290
|
-
CONFIG_FILE="$CONFIG_DIR/agent.env"
|
|
291
|
-
|
|
292
|
-
# Colors
|
|
293
|
-
BOLD="\033[1m"
|
|
294
|
-
GREEN="\033[0;32m"
|
|
295
|
-
CYAN="\033[0;36m"
|
|
296
|
-
YELLOW="\033[1;33m"
|
|
297
|
-
BLUE="\033[0;34m"
|
|
298
|
-
MAGENTA="\033[0;35m"
|
|
299
|
-
RESET="\033[0m"
|
|
300
|
-
|
|
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
|
|
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"
|
|
570
|
-
fi
|
|
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
|
|
582
|
-
fi
|
|
583
|
-
return 1
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
ensure_dependencies() {
|
|
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
|
|
601
|
-
fi
|
|
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
|
|
614
|
-
fi
|
|
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
|
|
670
|
-
fi
|
|
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
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
# Parse CLI args
|
|
694
|
-
while [[ $# -gt 0 ]]; do
|
|
695
|
-
case "$1" in
|
|
696
|
-
--lang=*)
|
|
697
|
-
LANG_CHOICE="${1#*=}"
|
|
698
|
-
EXPLICIT_LANG=true
|
|
699
|
-
shift
|
|
700
|
-
;;
|
|
701
|
-
-q|--quick)
|
|
702
|
-
QUICK=true
|
|
703
|
-
shift
|
|
704
|
-
;;
|
|
705
|
-
--mode=*)
|
|
706
|
-
MODE="${1#*=}"
|
|
707
|
-
shift
|
|
708
|
-
;;
|
|
709
|
-
--user=*)
|
|
710
|
-
USERNAME="${1#*=}"
|
|
711
|
-
shift
|
|
712
|
-
;;
|
|
713
|
-
--token=*)
|
|
714
|
-
TOKEN="${1#*=}"
|
|
715
|
-
shift
|
|
716
|
-
;;
|
|
717
|
-
--gateway=*)
|
|
718
|
-
# Historical alias of --domain=: both are the explicit override.
|
|
719
|
-
DOMAIN="${1#*=}"
|
|
720
|
-
shift
|
|
721
|
-
;;
|
|
722
|
-
--port=*)
|
|
723
|
-
PORT="${1#*=}"
|
|
724
|
-
shift
|
|
725
|
-
;;
|
|
726
|
-
--ssh=*)
|
|
727
|
-
SSH_ARG="${1#*=}"
|
|
728
|
-
if [[ "$SSH_ARG" =~ ^([^:]+):([0-9]+)$ ]]; then
|
|
729
|
-
SSH_TARGET="${BASH_REMATCH[1]}"
|
|
730
|
-
SSH_PORT="${BASH_REMATCH[2]}"
|
|
731
|
-
else
|
|
732
|
-
SSH_TARGET="$SSH_ARG"
|
|
733
|
-
fi
|
|
734
|
-
shift
|
|
735
|
-
;;
|
|
736
|
-
--tls=*)
|
|
737
|
-
TLS="${1#*=}"
|
|
738
|
-
shift
|
|
739
|
-
;;
|
|
740
|
-
--domain=*)
|
|
741
|
-
# The shared public domain that serves every node. This is NOT a
|
|
742
|
-
# per-device subdomain: nodes are selected with ?user=<node>.
|
|
743
|
-
DOMAIN="${1#*=}"
|
|
744
|
-
GATEWAY="$DOMAIN"
|
|
745
|
-
shift
|
|
746
|
-
;;
|
|
747
|
-
--dry-run)
|
|
748
|
-
DRY_RUN=true
|
|
749
|
-
shift
|
|
750
|
-
;;
|
|
751
|
-
-h|--help)
|
|
752
|
-
echo "Usage: $0 [--lang=en|ru] [-q|--quick] [--mode=tunnel|standalone|gateway] [--user=<node-name>] [--token=<token>] [--domain=<shared-domain>] [--port=<port>] [--ssh=user@host] [--dry-run]"
|
|
753
|
-
echo ""
|
|
754
|
-
echo "All nodes use ONE shared domain; the node is selected by ?user=<node-name>:"
|
|
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)."
|
|
765
|
-
exit 0
|
|
766
|
-
;;
|
|
767
|
-
*)
|
|
768
|
-
echo -e "${YELLOW}[ERROR] Unknown parameter: $1${RESET}"
|
|
769
|
-
exit 1
|
|
770
|
-
;;
|
|
771
|
-
esac
|
|
772
|
-
done
|
|
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
|
-
|
|
913
|
-
# ------------------------------------------------------------------------------
|
|
914
|
-
# SHARED-DOMAIN CONTRACT (canonical step)
|
|
915
|
-
# Every node is reached through ONE public domain ($GATEWAY); the node is
|
|
916
|
-
# selected by the ?user= query parameter. The installer never mints a
|
|
917
|
-
# per-device subdomain: each extra hostname would need its own DNS record and
|
|
918
|
-
# TLS SAN, and a name missing from the certificate fails the handshake with an
|
|
919
|
-
# opaque "cannot connect to host" error in the Gemini client.
|
|
920
|
-
# sse https://<shared-domain>/sse?user=<node-name>&token=<token>
|
|
921
|
-
# http https://<shared-domain>/mcp?user=<node-name>&token=<token>
|
|
922
|
-
# tunnel wss://<shared-domain>/ws/tunnel?user=<node-name>&token=<token>
|
|
923
|
-
# $GATEWAY is read at call time so the interactive menu can still change it.
|
|
924
|
-
# ------------------------------------------------------------------------------
|
|
925
|
-
node_sse_url() { printf 'https://%s/sse?user=%s&token=%s' "$GATEWAY" "$1" "$2"; }
|
|
926
|
-
|
|
927
|
-
# Language prompt if interactive and not specified
|
|
928
|
-
if [ -z "$LANG_CHOICE" ]; then
|
|
929
|
-
if [ "$QUICK" = false ] && [ -z "$MODE" ] && [ -t 0 ]; then
|
|
930
|
-
echo -e "\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
|
|
931
|
-
echo -e "${BOLD}${CYAN}║ 🌐 LANGUAGE SELECTION / ВЫБОР ЯЗЫКА ║${RESET}"
|
|
932
|
-
echo -e "${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
|
|
933
|
-
echo -e " ${GREEN}1)${RESET} English (Default)"
|
|
934
|
-
echo -e " ${BLUE}2)${RESET} Русский"
|
|
935
|
-
read -rp "Select / Выберите [1]: " LANG_INPUT
|
|
936
|
-
if [ "$LANG_INPUT" = "2" ] || [ "$LANG_INPUT" = "ru" ] || [ "$LANG_INPUT" = "RU" ]; then
|
|
937
|
-
LANG_CHOICE="ru"
|
|
938
|
-
else
|
|
939
|
-
LANG_CHOICE="en"
|
|
940
|
-
fi
|
|
941
|
-
else
|
|
942
|
-
LANG_CHOICE="en"
|
|
943
|
-
fi
|
|
944
|
-
fi
|
|
945
|
-
|
|
946
|
-
# Device detection
|
|
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
|
-
}
|
|
978
|
-
|
|
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
|
|
1012
|
-
DETECTED_OS="macOS $(sw_vers -productVersion 2>/dev/null || '')"
|
|
1013
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1014
|
-
DETECTED_TYPE="Apple Mac"
|
|
1015
|
-
sysctl -n hw.model 2>/dev/null | grep -qi "book" && DETECTED_TYPE="Apple MacBook (Ноутбук)"
|
|
1016
|
-
else
|
|
1017
|
-
DETECTED_TYPE="Apple Mac"
|
|
1018
|
-
sysctl -n hw.model 2>/dev/null | grep -qi "book" && DETECTED_TYPE="Apple MacBook (Laptop)"
|
|
1019
|
-
fi
|
|
1020
|
-
elif [ -f /etc/os-release ]; then
|
|
1021
|
-
# shellcheck disable=SC1091
|
|
1022
|
-
. /etc/os-release
|
|
1023
|
-
DETECTED_OS="${PRETTY_NAME:-$NAME}"
|
|
1024
|
-
else
|
|
1025
|
-
DETECTED_OS=$(uname -s)
|
|
1026
|
-
fi
|
|
1027
|
-
|
|
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.
|
|
1034
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1035
|
-
DETECTED_TYPE="Десктоп / Сервер"
|
|
1036
|
-
if grep -q -i "microsoft" /proc/version 2>/dev/null; then
|
|
1037
|
-
DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
|
|
1038
|
-
elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
|
|
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)"
|
|
1042
|
-
elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
|
|
1043
|
-
DETECTED_TYPE="Облачный сервер / VPS ($(systemd-detect-virt))"
|
|
1044
|
-
fi
|
|
1045
|
-
else
|
|
1046
|
-
DETECTED_TYPE="Desktop / Server"
|
|
1047
|
-
if grep -q -i "microsoft" /proc/version 2>/dev/null; then
|
|
1048
|
-
DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
|
|
1049
|
-
elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
|
|
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"
|
|
1053
|
-
elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
|
|
1054
|
-
DETECTED_TYPE="Cloud VPS ($(systemd-detect-virt))"
|
|
1055
|
-
fi
|
|
1056
|
-
fi
|
|
1057
|
-
fi
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
detect_device
|
|
1061
|
-
|
|
1062
|
-
print_device_info() {
|
|
1063
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1064
|
-
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
|
|
1065
|
-
echo -e "${CYAN}║${RESET} ${BOLD}🔍 Обнаружено устройство:${RESET}"
|
|
1066
|
-
echo -e "${CYAN}║${RESET} • Имя хоста : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
|
|
1067
|
-
echo -e "${CYAN}║${RESET} • Тип : ${YELLOW}${DETECTED_TYPE}${RESET}"
|
|
1068
|
-
echo -e "${CYAN}║${RESET} • ОС : ${DETECTED_OS}"
|
|
1069
|
-
echo -e "${CYAN}║${RESET} • Архитектура : ${ARCH_LABEL}"
|
|
1070
|
-
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
|
|
1071
|
-
else
|
|
1072
|
-
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
|
|
1073
|
-
echo -e "${CYAN}║${RESET} ${BOLD}🔍 Detected Device:${RESET}"
|
|
1074
|
-
echo -e "${CYAN}║${RESET} • Hostname : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
|
|
1075
|
-
echo -e "${CYAN}║${RESET} • Type : ${YELLOW}${DETECTED_TYPE}${RESET}"
|
|
1076
|
-
echo -e "${CYAN}║${RESET} • OS : ${DETECTED_OS}"
|
|
1077
|
-
echo -e "${CYAN}║${RESET} • Architecture: ${ARCH_LABEL}"
|
|
1078
|
-
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
|
|
1079
|
-
fi
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
print_mcp_box() {
|
|
1083
|
-
local title="$1"
|
|
1084
|
-
local url="$2"
|
|
1085
|
-
local copy_msg="$3"
|
|
1086
|
-
|
|
1087
|
-
local max_len=${#title}
|
|
1088
|
-
[ ${#url} -gt $max_len ] && max_len=${#url}
|
|
1089
|
-
[ -n "$copy_msg" ] && [ ${#copy_msg} -gt $max_len ] && max_len=${#copy_msg}
|
|
1090
|
-
|
|
1091
|
-
local width=$(( max_len + 4 ))
|
|
1092
|
-
[ $width -lt 84 ] && width=84
|
|
1093
|
-
|
|
1094
|
-
local dashes=$(printf '%*s' "$width" '' | tr ' ' '-')
|
|
1095
|
-
local border=" +${dashes}+"
|
|
1096
|
-
local empty=" |$(printf '%*s' "$width" '')|"
|
|
1097
|
-
|
|
1098
|
-
echo -e "${GREEN}${border}${RESET}"
|
|
1099
|
-
|
|
1100
|
-
# Title
|
|
1101
|
-
local pad_title=$(( width - 2 - ${#title} ))
|
|
1102
|
-
printf " ${GREEN}|${RESET} ${BOLD}%s${RESET}%*s ${GREEN}|${RESET}\n" "$title" "$pad_title" ""
|
|
1103
|
-
|
|
1104
|
-
echo -e "${GREEN}${empty}${RESET}"
|
|
1105
|
-
|
|
1106
|
-
# URL
|
|
1107
|
-
local pad_url=$(( width - 2 - ${#url} ))
|
|
1108
|
-
printf " ${GREEN}|${RESET} ${BOLD}${YELLOW}%s${RESET}%*s ${GREEN}|${RESET}\n" "$url" "$pad_url" ""
|
|
1109
|
-
|
|
1110
|
-
echo -e "${GREEN}${empty}${RESET}"
|
|
1111
|
-
|
|
1112
|
-
# Copy message
|
|
1113
|
-
if [ -n "$copy_msg" ]; then
|
|
1114
|
-
local pad_copy=$(( width - 2 - ${#copy_msg} ))
|
|
1115
|
-
printf " ${GREEN}|${RESET} ${BOLD}${GREEN}%s${RESET}%*s ${GREEN}|${RESET}\n" "$copy_msg" "$pad_copy" ""
|
|
1116
|
-
fi
|
|
1117
|
-
|
|
1118
|
-
echo -e "${GREEN}${border}${RESET}"
|
|
1119
|
-
}
|
|
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
|
-
|
|
1334
|
-
# Dry-run
|
|
1335
|
-
if [ "$DRY_RUN" = true ]; then
|
|
1336
|
-
echo "[DRY-RUN] Simulating Antigravity Mesh installation..."
|
|
1337
|
-
echo "[DRY-RUN] Language: $LANG_CHOICE"
|
|
1338
|
-
echo "[DRY-RUN] Detected Host: $DETECTED_HOSTNAME ($DETECTED_TYPE)"
|
|
1339
|
-
echo "[DRY-RUN] Architecture: ${ARCH_LABEL:-$DETECTED_ARCH}"
|
|
1340
|
-
echo "[DRY-RUN] Selected Mode: ${MODE:-standalone}"
|
|
1341
|
-
echo "[DRY-RUN] Port: $PORT"
|
|
1342
|
-
echo "[DRY-RUN] TLS: $TLS"
|
|
1343
|
-
if [ -n "$GATEWAY" ]; then echo "[DRY-RUN] Domain: $GATEWAY"; fi
|
|
1344
|
-
if [ -n "$USERNAME" ]; then
|
|
1345
|
-
echo "[DRY-RUN] User: $USERNAME"
|
|
1346
|
-
if [ -n "$GATEWAY" ]; then
|
|
1347
|
-
echo "[DRY-RUN] Canonical MCP URL: $(node_sse_url "$USERNAME" "${TOKEN:-<token>}")"
|
|
1348
|
-
fi
|
|
1349
|
-
fi
|
|
1350
|
-
if [ -n "$TOKEN" ]; then echo "[DRY-RUN] Token: set (standalone: stored in ${CONFIG_DIR}/standalone.env, chmod 600)"; fi
|
|
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
|
|
1368
|
-
exit 0
|
|
1369
|
-
fi
|
|
1370
|
-
|
|
1371
|
-
# SSH Remote install branch
|
|
1372
|
-
if [ -n "$SSH_TARGET" ]; then
|
|
1373
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1374
|
-
echo -e "${BOLD}${BLUE}=== Удаленная установка Antigravity Mesh через SSH ===${RESET}"
|
|
1375
|
-
echo -e "Целевой сервер: ${CYAN}${SSH_TARGET}${RESET} (порт: ${SSH_PORT})"
|
|
1376
|
-
else
|
|
1377
|
-
echo -e "${BOLD}${BLUE}=== Remote Antigravity Mesh SSH Installation ===${RESET}"
|
|
1378
|
-
echo -e "Target Server: ${CYAN}${SSH_TARGET}${RESET} (port: ${SSH_PORT})"
|
|
1379
|
-
fi
|
|
1380
|
-
|
|
1381
|
-
SSH_BIN="ssh -o StrictHostKeyChecking=no"
|
|
1382
|
-
SCP_BIN="scp -o StrictHostKeyChecking=no"
|
|
1383
|
-
if [ -n "$SSHPASS" ] && command -v sshpass >/dev/null 2>&1; then
|
|
1384
|
-
SSH_BIN="sshpass -e $SSH_BIN"
|
|
1385
|
-
SCP_BIN="sshpass -e $SCP_BIN"
|
|
1386
|
-
fi
|
|
1387
|
-
|
|
1388
|
-
$SSH_BIN -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p ~/antigravity-mesh/core ~/antigravity-mesh/skills"
|
|
1389
|
-
$SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/core"/* "$SSH_TARGET:~/antigravity-mesh/core/"
|
|
1390
|
-
$SCP_BIN -P "$SSH_PORT" "$SCRIPT_DIR/install.sh" "$SSH_TARGET:~/antigravity-mesh/"
|
|
1391
|
-
if [ -d "$SCRIPT_DIR/skills" ]; then
|
|
1392
|
-
$SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/skills"/* "$SSH_TARGET:~/antigravity-mesh/skills/" 2>/dev/null || true
|
|
1393
|
-
fi
|
|
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"
|
|
1403
|
-
exit 0
|
|
1404
|
-
fi
|
|
1405
|
-
|
|
1406
|
-
# Interactive Menu if no mode is specified and not in quick mode
|
|
1407
|
-
if [ "$QUICK" = false ] && [ -z "$MODE" ] && [ -t 0 ]; then
|
|
1408
|
-
print_device_info
|
|
1409
|
-
echo ""
|
|
1410
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1411
|
-
echo -e "${BOLD}Выберите режим установки:${RESET}"
|
|
1412
|
-
echo -e " ${GREEN}1)${RESET} ⚡ ${BOLD}Быстрая настройка${RESET} (Имя узла как имя ПК + Общий домен + Автозапуск) [Рекомендуется]"
|
|
1413
|
-
echo -e " ${BLUE}2)${RESET} 🖥️ ${BOLD}Локальный Standalone${RESET} (Только localhost:${PORT}, без облачного шлюза)"
|
|
1414
|
-
echo -e " ${YELLOW}3)${RESET} ⚙️ ${BOLD}Кастомная настройка${RESET} (Ввести имя узла вручную, выбор общего домена)"
|
|
1415
|
-
echo -e " ${CYAN}4)${RESET} 📡 ${BOLD}Удаленная установка на SSH-сервер${RESET}"
|
|
1416
|
-
echo -e " ${RESET}0) Выход"
|
|
1417
|
-
echo ""
|
|
1418
|
-
read -rp "Ваш выбор [1]: " MENU_CHOICE
|
|
1419
|
-
else
|
|
1420
|
-
echo -e "${BOLD}Select Installation Mode:${RESET}"
|
|
1421
|
-
echo -e " ${GREEN}1)${RESET} ⚡ ${BOLD}Quick Setup${RESET} (Node name from PC name + shared domain + Autostart) [Recommended]"
|
|
1422
|
-
echo -e " ${BLUE}2)${RESET} 🖥️ ${BOLD}Local Standalone${RESET} (localhost:${PORT} only, no cloud gateway)"
|
|
1423
|
-
echo -e " ${YELLOW}3)${RESET} ⚙️ ${BOLD}Custom Setup${RESET} (Custom node name, custom shared domain)"
|
|
1424
|
-
echo -e " ${CYAN}4)${RESET} 📡 ${BOLD}Remote SSH Installation${RESET}"
|
|
1425
|
-
echo -e " ${RESET}0) Exit"
|
|
1426
|
-
echo ""
|
|
1427
|
-
read -rp "Select [1]: " MENU_CHOICE
|
|
1428
|
-
fi
|
|
1429
|
-
MENU_CHOICE=${MENU_CHOICE:-1}
|
|
1430
|
-
|
|
1431
|
-
case "$MENU_CHOICE" in
|
|
1432
|
-
1)
|
|
1433
|
-
MODE="tunnel"
|
|
1434
|
-
QUICK=true
|
|
1435
|
-
;;
|
|
1436
|
-
2)
|
|
1437
|
-
MODE="standalone"
|
|
1438
|
-
;;
|
|
1439
|
-
3)
|
|
1440
|
-
MODE="tunnel"
|
|
1441
|
-
echo ""
|
|
1442
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1443
|
-
read -rp "Введите имя узла [по умолчанию: ${DETECTED_HOSTNAME}]: " CUSTOM_SUB
|
|
1444
|
-
read -rp "Общий домен шлюза [по умолчанию: ${GATEWAY}]: " CUSTOM_GW
|
|
1445
|
-
else
|
|
1446
|
-
read -rp "Enter node name [default: ${DETECTED_HOSTNAME}]: " CUSTOM_SUB
|
|
1447
|
-
read -rp "Shared gateway domain [default: ${GATEWAY}]: " CUSTOM_GW
|
|
1448
|
-
fi
|
|
1449
|
-
if [ -n "$CUSTOM_SUB" ]; then USERNAME="$CUSTOM_SUB"; fi
|
|
1450
|
-
if [ -n "$CUSTOM_GW" ]; then GATEWAY="$CUSTOM_GW"; fi
|
|
1451
|
-
;;
|
|
1452
|
-
4)
|
|
1453
|
-
echo ""
|
|
1454
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1455
|
-
read -rp "Введите SSH цель (например, user@<host-ip>): " REMOTE_TARGET
|
|
1456
|
-
read -rp "Порт SSH [22]: " REMOTE_PORT
|
|
1457
|
-
else
|
|
1458
|
-
read -rp "Enter SSH target (e.g., user@<host-ip>): " REMOTE_TARGET
|
|
1459
|
-
read -rp "SSH Port [22]: " REMOTE_PORT
|
|
1460
|
-
fi
|
|
1461
|
-
REMOTE_PORT=${REMOTE_PORT:-22}
|
|
1462
|
-
exec "$0" "--ssh=${REMOTE_TARGET}:${REMOTE_PORT}" "--lang=${LANG_CHOICE}"
|
|
1463
|
-
;;
|
|
1464
|
-
0)
|
|
1465
|
-
echo "Cancelled."
|
|
1466
|
-
exit 0
|
|
1467
|
-
;;
|
|
1468
|
-
*)
|
|
1469
|
-
MODE="tunnel"
|
|
1470
|
-
QUICK=true
|
|
1471
|
-
;;
|
|
1472
|
-
esac
|
|
1473
|
-
fi
|
|
1474
|
-
|
|
1475
|
-
if [ -z "$MODE" ]; then
|
|
1476
|
-
MODE="tunnel"
|
|
1477
|
-
fi
|
|
1478
|
-
|
|
1479
|
-
print_device_info
|
|
1480
|
-
|
|
1481
|
-
# ==============================================================================
|
|
1482
|
-
# BRANCH: STANDALONE MODE
|
|
1483
|
-
# ==============================================================================
|
|
1484
|
-
if [ "$MODE" = "standalone" ]; then
|
|
1485
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1486
|
-
echo -e "\n${BOLD}${BLUE}=== Настройка локального Standalone FastMCP сервера ===${RESET}"
|
|
1487
|
-
echo "[1/3] Проверка окружения Python..."
|
|
1488
|
-
else
|
|
1489
|
-
echo -e "\n${BOLD}${BLUE}=== Setting up Local Standalone FastMCP Server ===${RESET}"
|
|
1490
|
-
echo "[1/3] Checking Python environment..."
|
|
1491
|
-
fi
|
|
1492
|
-
ensure_dependencies
|
|
1493
|
-
# The endpoint check at the end of this branch runs through curl.
|
|
1494
|
-
ensure_curl || exit 1
|
|
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"
|
|
1499
|
-
|
|
1500
|
-
# Honour --token in standalone mode without embedding the secret in the
|
|
1501
|
-
# (world-readable) unit/plist: keep it in a 0600 env file for systemd
|
|
1502
|
-
# (EnvironmentFile) and use a 0600 plist environment block on macOS.
|
|
1503
|
-
# core.server reads MESH_TOKEN as the default for --token.
|
|
1504
|
-
STANDALONE_ENV_FILE="$CONFIG_DIR/standalone.env"
|
|
1505
|
-
ENV_FILE_LINE=""
|
|
1506
|
-
PLIST_ENV_BLOCK=""
|
|
1507
|
-
if [ -n "$TOKEN" ]; then
|
|
1508
|
-
printf 'MESH_TOKEN=%s\n' "$TOKEN" > "$STANDALONE_ENV_FILE"
|
|
1509
|
-
chmod 600 "$STANDALONE_ENV_FILE"
|
|
1510
|
-
ENV_FILE_LINE="EnvironmentFile=$STANDALONE_ENV_FILE"
|
|
1511
|
-
PLIST_TOKEN=$(printf '%s' "$TOKEN" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g')
|
|
1512
|
-
PLIST_ENV_BLOCK=" <key>EnvironmentVariables</key>
|
|
1513
|
-
<dict>
|
|
1514
|
-
<key>MESH_TOKEN</key>
|
|
1515
|
-
<string>${PLIST_TOKEN}</string>
|
|
1516
|
-
</dict>"
|
|
1517
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1518
|
-
echo -e "${GREEN}[✓] Токен авторизации включён (chmod 600).${RESET}"
|
|
1519
|
-
else
|
|
1520
|
-
echo -e "${GREEN}[OK] Authorization token enabled (chmod 600).${RESET}"
|
|
1521
|
-
fi
|
|
1522
|
-
else
|
|
1523
|
-
rm -f "$STANDALONE_ENV_FILE" 2>/dev/null || true
|
|
1524
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1525
|
-
echo -e "${YELLOW}[!] Токен не задан (--token): сервер доступен без авторизации на 127.0.0.1.${RESET}"
|
|
1526
|
-
else
|
|
1527
|
-
echo -e "${YELLOW}[!] No --token given: server is unauthenticated on 127.0.0.1.${RESET}"
|
|
1528
|
-
fi
|
|
1529
|
-
fi
|
|
1530
|
-
|
|
1531
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1532
|
-
if [ "$IS_TERMUX" = true ]; then
|
|
1533
|
-
echo "[2/3] Настройка автозапуска на телефоне (runit + Termux:Boot)..."
|
|
1534
|
-
else
|
|
1535
|
-
echo "[2/3] Настройка systemd автозапуска на ПК..."
|
|
1536
|
-
fi
|
|
1537
|
-
else
|
|
1538
|
-
echo "[2/3] Configuring autostart background service..."
|
|
1539
|
-
fi
|
|
1540
|
-
|
|
1541
|
-
if [ "$(uname -s)" = "Darwin" ]; then
|
|
1542
|
-
LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
|
|
1543
|
-
mkdir -p "$LAUNCH_AGENTS"
|
|
1544
|
-
PLIST_FILE="$LAUNCH_AGENTS/com.antigravity.mesh.standalone.plist"
|
|
1545
|
-
cat << EOF > "$PLIST_FILE"
|
|
1546
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1547
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1548
|
-
<plist version="1.0">
|
|
1549
|
-
<dict>
|
|
1550
|
-
<key>Label</key>
|
|
1551
|
-
<string>com.antigravity.mesh.standalone</string>
|
|
1552
|
-
<key>ProgramArguments</key>
|
|
1553
|
-
<array>
|
|
1554
|
-
<string>$PYTHON_BIN</string>
|
|
1555
|
-
<string>-m</string>
|
|
1556
|
-
<string>core.server</string>
|
|
1557
|
-
<string>--host=127.0.0.1</string>
|
|
1558
|
-
<string>--port=$PORT</string>
|
|
1559
|
-
</array>
|
|
1560
|
-
<key>WorkingDirectory</key>
|
|
1561
|
-
<string>$SCRIPT_DIR</string>
|
|
1562
|
-
<key>RunAtLoad</key>
|
|
1563
|
-
<true/>
|
|
1564
|
-
<key>KeepAlive</key>
|
|
1565
|
-
<true/>
|
|
1566
|
-
$PLIST_ENV_BLOCK
|
|
1567
|
-
</dict>
|
|
1568
|
-
</plist>
|
|
1569
|
-
EOF
|
|
1570
|
-
if [ -n "$TOKEN" ]; then
|
|
1571
|
-
chmod 600 "$PLIST_FILE"
|
|
1572
|
-
fi
|
|
1573
|
-
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
|
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"
|
|
1579
|
-
else
|
|
1580
|
-
USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
|
|
1581
|
-
mkdir -p "$USER_SYSTEMD_DIR"
|
|
1582
|
-
SERVICE_FILE="$USER_SYSTEMD_DIR/agy-standalone.service"
|
|
1583
|
-
cat << EOF > "$SERVICE_FILE"
|
|
1584
|
-
[Unit]
|
|
1585
|
-
Description=Antigravity Mesh Local Standalone MCP Server
|
|
1586
|
-
After=network.target
|
|
1587
|
-
|
|
1588
|
-
[Service]
|
|
1589
|
-
Type=simple
|
|
1590
|
-
$ENV_FILE_LINE
|
|
1591
|
-
WorkingDirectory=$SCRIPT_DIR
|
|
1592
|
-
ExecStart=$PYTHON_BIN -m core.server --host 127.0.0.1 --port=$PORT
|
|
1593
|
-
Restart=always
|
|
1594
|
-
RestartSec=5
|
|
1595
|
-
|
|
1596
|
-
[Install]
|
|
1597
|
-
WantedBy=default.target
|
|
1598
|
-
EOF
|
|
1599
|
-
systemctl --user daemon-reload 2>/dev/null || true
|
|
1600
|
-
systemctl --user enable --now agy-standalone.service 2>/dev/null || {
|
|
1601
|
-
pkill -f "core.server" 2>/dev/null || true
|
|
1602
|
-
if [ -n "$TOKEN" ]; then
|
|
1603
|
-
nohup env MESH_TOKEN="$TOKEN" $PYTHON_BIN -m core.server --host 127.0.0.1 --port="$PORT" > "$CONFIG_DIR/standalone.log" 2>&1 &
|
|
1604
|
-
else
|
|
1605
|
-
nohup $PYTHON_BIN -m core.server --host 127.0.0.1 --port="$PORT" > "$CONFIG_DIR/standalone.log" 2>&1 &
|
|
1606
|
-
fi
|
|
1607
|
-
}
|
|
1608
|
-
loginctl enable-linger "$USER" 2>/dev/null || true
|
|
1609
|
-
fi
|
|
1610
|
-
|
|
1611
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1612
|
-
echo "[3/3] Проверка локального эндпоинта..."
|
|
1613
|
-
else
|
|
1614
|
-
echo "[3/3] Verifying local endpoint..."
|
|
1615
|
-
fi
|
|
1616
|
-
HEALTH_URL="http://127.0.0.1:${PORT}/health"
|
|
1617
|
-
HEALTH_OK=false
|
|
1618
|
-
for _attempt in 1 2 3 4 5 6 7 8 9 10; do
|
|
1619
|
-
HTTP_CODE=$(curl -s -o /dev/null -m 2 -w "%{http_code}" "$HEALTH_URL" 2>/dev/null || true)
|
|
1620
|
-
if [ "$HTTP_CODE" = "200" ]; then
|
|
1621
|
-
HEALTH_OK=true
|
|
1622
|
-
break
|
|
1623
|
-
fi
|
|
1624
|
-
sleep 1
|
|
1625
|
-
done
|
|
1626
|
-
if [ "$HEALTH_OK" != true ]; then
|
|
1627
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1628
|
-
echo -e "\033[1;31m[ОШИБКА] Standalone MCP-сервер не отвечает (HTTP ${HTTP_CODE:-нет ответа}): $HEALTH_URL\033[0m" >&2
|
|
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
|
|
1638
|
-
else
|
|
1639
|
-
echo -e "\033[1;31m[ERROR] Standalone MCP server is not responding (HTTP ${HTTP_CODE:-no response}): $HEALTH_URL\033[0m" >&2
|
|
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
|
|
1649
|
-
fi
|
|
1650
|
-
exit 1
|
|
1651
|
-
fi
|
|
1652
|
-
|
|
1653
|
-
MCP_LOCAL_URL="http://localhost:${PORT}/sse"
|
|
1654
|
-
|
|
1655
|
-
# Quick Copy to Clipboard
|
|
1656
|
-
COPIED=false
|
|
1657
|
-
B64_URL=$(printf "%s" "$MCP_LOCAL_URL" | base64 | tr -d '\r\n')
|
|
1658
|
-
printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
|
|
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
|
|
1666
|
-
printf "%s" "$MCP_LOCAL_URL" | wl-copy 2>/dev/null && COPIED=true
|
|
1667
|
-
elif command -v xclip >/dev/null 2>&1; then
|
|
1668
|
-
printf "%s" "$MCP_LOCAL_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
|
|
1669
|
-
elif command -v xsel >/dev/null 2>&1; then
|
|
1670
|
-
printf "%s" "$MCP_LOCAL_URL" | xsel --clipboard --input 2>/dev/null && COPIED=true
|
|
1671
|
-
elif command -v pbcopy >/dev/null 2>&1; then
|
|
1672
|
-
printf "%s" "$MCP_LOCAL_URL" | pbcopy 2>/dev/null && COPIED=true
|
|
1673
|
-
elif command -v clip.exe >/dev/null 2>&1; then
|
|
1674
|
-
printf "%s" "$MCP_LOCAL_URL" | clip.exe 2>/dev/null && COPIED=true
|
|
1675
|
-
fi
|
|
1676
|
-
|
|
1677
|
-
clear 2>/dev/null || printf "\033[2J\033[H" || true
|
|
1678
|
-
|
|
1679
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1680
|
-
echo ""
|
|
1681
|
-
copy_msg=""
|
|
1682
|
-
if [ "$COPIED" = true ]; then
|
|
1683
|
-
copy_msg="[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)"
|
|
1684
|
-
else
|
|
1685
|
-
copy_msg="Скопируйте ссылку выше"
|
|
1686
|
-
fi
|
|
1687
|
-
print_mcp_box "ССЫЛКА ЛОКАЛЬНОГО MCP-СЕРВЕРА (СКОПИРУЙТЕ):" "$MCP_LOCAL_URL" "$copy_msg"
|
|
1688
|
-
echo ""
|
|
1689
|
-
echo -e " ${BOLD}Куда вставлять ссылку в Google Gemini:${RESET}"
|
|
1690
|
-
echo -e " 1. Откройте в браузере: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
|
|
1691
|
-
echo -e " 2. Нажмите ${BOLD}'Добавить приложение'${RESET} (Add app / Настройки MCP)"
|
|
1692
|
-
echo -e " 3. Вставьте скопированную ссылку в поле ${BOLD}'URL сервера'${RESET} (${BOLD}Ctrl+V${RESET}) и нажмите ${BOLD}Подключить${RESET}."
|
|
1693
|
-
echo ""
|
|
1694
|
-
else
|
|
1695
|
-
echo ""
|
|
1696
|
-
copy_msg=""
|
|
1697
|
-
if [ "$COPIED" = true ]; then
|
|
1698
|
-
copy_msg="[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)"
|
|
1699
|
-
else
|
|
1700
|
-
copy_msg="Copy the URL above"
|
|
1701
|
-
fi
|
|
1702
|
-
print_mcp_box "LOCAL MCP SERVER URL (COPY THIS):" "$MCP_LOCAL_URL" "$copy_msg"
|
|
1703
|
-
echo ""
|
|
1704
|
-
echo -e " ${BOLD}Where to paste this URL in Google Gemini:${RESET}"
|
|
1705
|
-
echo -e " 1. Open in your browser: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
|
|
1706
|
-
echo -e " 2. Click ${BOLD}'Add app'${RESET} (or navigate to MCP settings)"
|
|
1707
|
-
echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
|
|
1708
|
-
echo ""
|
|
1709
|
-
fi
|
|
1710
|
-
if [ "$IS_TERMUX" = true ]; then
|
|
1711
|
-
print_termux_hints "agy-standalone"
|
|
1712
|
-
fi
|
|
1713
|
-
exit 0
|
|
1714
|
-
fi
|
|
1715
|
-
|
|
1716
|
-
# ==============================================================================
|
|
1717
|
-
# BRANCH: CLOUD GATEWAY + REVERSE TUNNEL
|
|
1718
|
-
# ==============================================================================
|
|
1719
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1720
|
-
echo -e "\n${BOLD}${MAGENTA}=== Настройка облачного туннеля (общий домен) ===${RESET}"
|
|
1721
|
-
else
|
|
1722
|
-
echo -e "\n${BOLD}${MAGENTA}=== Configuring Cloud Gateway Tunnel (shared domain) ===${RESET}"
|
|
1723
|
-
fi
|
|
1724
|
-
|
|
1725
|
-
if [ -z "$USERNAME" ]; then
|
|
1726
|
-
CLEAN_HOST=$(echo "$DETECTED_HOSTNAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
|
|
1727
|
-
USERNAME="$CLEAN_HOST"
|
|
1728
|
-
fi
|
|
1729
|
-
USERNAME=$(echo "$USERNAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
|
|
1730
|
-
[ -z "$USERNAME" ] && USERNAME="mesh-node"
|
|
1731
|
-
|
|
1732
|
-
mkdir -p "$CONFIG_DIR"
|
|
1733
|
-
|
|
1734
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1735
|
-
echo "[1/4] Проверка Python зависимостей (websockets)..."
|
|
1736
|
-
else
|
|
1737
|
-
echo "[1/4] Checking Python dependencies (websockets)..."
|
|
1738
|
-
fi
|
|
1739
|
-
|
|
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
|
|
1749
|
-
|
|
1750
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1751
|
-
echo "[2/4] Регистрация узла '${USERNAME}' на общем домене (${GATEWAY})..."
|
|
1752
|
-
else
|
|
1753
|
-
echo "[2/4] Registering node '${USERNAME}' on the shared domain (${GATEWAY})..."
|
|
1754
|
-
fi
|
|
1755
|
-
|
|
1756
|
-
EXISTING_TOKEN=""
|
|
1757
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
1758
|
-
CFG_USER=$(grep "MESH_USER" "$CONFIG_FILE" 2>/dev/null | cut -d '=' -f2 || true)
|
|
1759
|
-
CFG_TOKEN=$(grep "MESH_TOKEN" "$CONFIG_FILE" 2>/dev/null | cut -d '=' -f2 || true)
|
|
1760
|
-
if [ "$CFG_USER" = "$USERNAME" ] && [ -n "$CFG_TOKEN" ]; then
|
|
1761
|
-
EXISTING_TOKEN="$CFG_TOKEN"
|
|
1762
|
-
fi
|
|
1763
|
-
fi
|
|
1764
|
-
|
|
1765
|
-
if [ -n "$TOKEN" ]; then
|
|
1766
|
-
REQ_TOKEN="$TOKEN"
|
|
1767
|
-
elif [ -n "$EXISTING_TOKEN" ]; then
|
|
1768
|
-
REQ_TOKEN="$EXISTING_TOKEN"
|
|
1769
|
-
else
|
|
1770
|
-
REQ_TOKEN=""
|
|
1771
|
-
fi
|
|
1772
|
-
|
|
1773
|
-
DETECTED_MAC=""
|
|
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)
|
|
1778
|
-
fi
|
|
1779
|
-
if [ -z "$DETECTED_MAC" ] || [ "$DETECTED_MAC" = "00:00:00:00:00:00" ]; then
|
|
1780
|
-
for iface in /sys/class/net/*; do
|
|
1781
|
-
if [ -f "$iface/address" ] && [ "$(cat "$iface/type" 2>/dev/null)" = "1" ]; then
|
|
1782
|
-
addr=$(cat "$iface/address" 2>/dev/null)
|
|
1783
|
-
if [ -n "$addr" ] && [ "$addr" != "00:00:00:00:00:00" ]; then
|
|
1784
|
-
DETECTED_MAC="$addr"
|
|
1785
|
-
break
|
|
1786
|
-
fi
|
|
1787
|
-
fi
|
|
1788
|
-
done
|
|
1789
|
-
fi
|
|
1790
|
-
|
|
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}\""
|
|
1804
|
-
if [ -n "$REQ_TOKEN" ]; then
|
|
1805
|
-
REG_BODY="${REG_BODY}, \"token\": \"${REQ_TOKEN}\""
|
|
1806
|
-
fi
|
|
1807
|
-
REG_BODY="${REG_BODY}}"
|
|
1808
|
-
|
|
1809
|
-
REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
|
|
1810
|
-
-H "Content-Type: application/json" \
|
|
1811
|
-
-d "$REG_BODY")
|
|
1812
|
-
|
|
1813
|
-
ASSIGNED_USER=$(json_field "$REG_RESP" username)
|
|
1814
|
-
ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
|
|
1815
|
-
|
|
1816
|
-
if [ -z "$ASSIGNED_TOKEN" ]; then
|
|
1817
|
-
FALLBACK_USER="${USERNAME}-$(date +%s | tail -c 4)"
|
|
1818
|
-
REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
|
|
1819
|
-
-H "Content-Type: application/json" \
|
|
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)
|
|
1823
|
-
fi
|
|
1824
|
-
|
|
1825
|
-
if [ -z "$ASSIGNED_TOKEN" ]; then
|
|
1826
|
-
echo -e "${RED}[ERROR] Failed to obtain authentication token from gateway.${RESET}"
|
|
1827
|
-
exit 1
|
|
1828
|
-
fi
|
|
1829
|
-
|
|
1830
|
-
if [ "$ASSIGNED_USER" != "$USERNAME" ]; then
|
|
1831
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1832
|
-
echo -e "${YELLOW}ℹ️ Имя '${USERNAME}' уже было занято. Автоматически назначено имя узла:${RESET} ${BOLD}${GREEN}${ASSIGNED_USER}${RESET}"
|
|
1833
|
-
else
|
|
1834
|
-
echo -e "${YELLOW}ℹ️ Name '${USERNAME}' was already taken. Automatically assigned node name:${RESET} ${BOLD}${GREEN}${ASSIGNED_USER}${RESET}"
|
|
1835
|
-
fi
|
|
1836
|
-
fi
|
|
1837
|
-
|
|
1838
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1839
|
-
echo "[3/4] Сохранение конфигурации в ${CONFIG_FILE}..."
|
|
1840
|
-
else
|
|
1841
|
-
echo "[3/4] Saving configuration to ${CONFIG_FILE}..."
|
|
1842
|
-
fi
|
|
1843
|
-
|
|
1844
|
-
cat << EOF > "$CONFIG_FILE"
|
|
1845
|
-
MESH_GATEWAY=${GATEWAY}
|
|
1846
|
-
MESH_USER=${ASSIGNED_USER}
|
|
1847
|
-
MESH_TOKEN=${ASSIGNED_TOKEN}
|
|
1848
|
-
EOF
|
|
1849
|
-
chmod 600 "$CONFIG_FILE"
|
|
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
|
-
|
|
1856
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1857
|
-
if [ "$IS_TERMUX" = true ]; then
|
|
1858
|
-
echo "[4/4] Настройка и запуск автозапуска на телефоне (runit + Termux:Boot)..."
|
|
1859
|
-
else
|
|
1860
|
-
echo "[4/4] Настройка и запуск службы автозапуска на ПК..."
|
|
1861
|
-
fi
|
|
1862
|
-
else
|
|
1863
|
-
echo "[4/4] Configuring and starting background autostart service..."
|
|
1864
|
-
fi
|
|
1865
|
-
|
|
1866
|
-
if [ "$(uname -s)" = "Darwin" ]; then
|
|
1867
|
-
LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
|
|
1868
|
-
mkdir -p "$LAUNCH_AGENTS"
|
|
1869
|
-
PLIST_FILE="$LAUNCH_AGENTS/com.antigravity.mesh.agent.plist"
|
|
1870
|
-
cat << EOF > "$PLIST_FILE"
|
|
1871
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1872
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1873
|
-
<plist version="1.0">
|
|
1874
|
-
<dict>
|
|
1875
|
-
<key>Label</key>
|
|
1876
|
-
<string>com.antigravity.mesh.agent</string>
|
|
1877
|
-
<key>ProgramArguments</key>
|
|
1878
|
-
<array>
|
|
1879
|
-
<string>$PYTHON_BIN</string>
|
|
1880
|
-
<string>-m</string>
|
|
1881
|
-
<string>core.agent</string>
|
|
1882
|
-
</array>
|
|
1883
|
-
<key>WorkingDirectory</key>
|
|
1884
|
-
<string>$SCRIPT_DIR</string>
|
|
1885
|
-
<key>EnvironmentVariables</key>
|
|
1886
|
-
<dict>
|
|
1887
|
-
<key>MESH_GATEWAY</key>
|
|
1888
|
-
<string>$GATEWAY</string>
|
|
1889
|
-
<key>MESH_USER</key>
|
|
1890
|
-
<string>$ASSIGNED_USER</string>
|
|
1891
|
-
<key>MESH_TOKEN</key>
|
|
1892
|
-
<string>$ASSIGNED_TOKEN</string>
|
|
1893
|
-
</dict>
|
|
1894
|
-
<key>RunAtLoad</key>
|
|
1895
|
-
<true/>
|
|
1896
|
-
<key>KeepAlive</key>
|
|
1897
|
-
<true/>
|
|
1898
|
-
</dict>
|
|
1899
|
-
</plist>
|
|
1900
|
-
EOF
|
|
1901
|
-
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
|
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"
|
|
1907
|
-
else
|
|
1908
|
-
USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
|
|
1909
|
-
mkdir -p "$USER_SYSTEMD_DIR"
|
|
1910
|
-
cat << EOF > "$USER_SYSTEMD_DIR/agy-agent.service"
|
|
1911
|
-
[Unit]
|
|
1912
|
-
Description=Antigravity Mesh Reverse RPC Agent
|
|
1913
|
-
After=network.target
|
|
1914
|
-
|
|
1915
|
-
[Service]
|
|
1916
|
-
Type=simple
|
|
1917
|
-
EnvironmentFile=$CONFIG_FILE
|
|
1918
|
-
WorkingDirectory=$SCRIPT_DIR
|
|
1919
|
-
ExecStart=$PYTHON_BIN -m core.agent
|
|
1920
|
-
Restart=always
|
|
1921
|
-
RestartSec=5
|
|
1922
|
-
|
|
1923
|
-
[Install]
|
|
1924
|
-
WantedBy=default.target
|
|
1925
|
-
EOF
|
|
1926
|
-
systemctl --user daemon-reload 2>/dev/null || true
|
|
1927
|
-
systemctl --user enable --now agy-agent.service 2>/dev/null || {
|
|
1928
|
-
pkill -f "core.agent" 2>/dev/null || true
|
|
1929
|
-
nohup $PYTHON_BIN -m core.agent > "$CONFIG_DIR/agent.log" 2>&1 &
|
|
1930
|
-
}
|
|
1931
|
-
loginctl enable-linger "$USER" 2>/dev/null || true
|
|
1932
|
-
fi
|
|
1933
|
-
|
|
1934
|
-
sleep 1
|
|
1935
|
-
|
|
1936
|
-
# Canonical shared-domain MCP URL (one domain for every node; ?user= selects it).
|
|
1937
|
-
MCP_URL="$(node_sse_url "$ASSIGNED_USER" "$ASSIGNED_TOKEN")"
|
|
1938
|
-
|
|
1939
|
-
# Quick Copy to Clipboard
|
|
1940
|
-
COPIED=false
|
|
1941
|
-
B64_URL=$(printf "%s" "$MCP_URL" | base64 | tr -d '\r\n')
|
|
1942
|
-
printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
|
|
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
|
|
1949
|
-
printf "%s" "$MCP_URL" | wl-copy 2>/dev/null && COPIED=true
|
|
1950
|
-
elif command -v xclip >/dev/null 2>&1; then
|
|
1951
|
-
printf "%s" "$MCP_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
|
|
1952
|
-
elif command -v xsel >/dev/null 2>&1; then
|
|
1953
|
-
printf "%s" "$MCP_URL" | xsel --clipboard --input 2>/dev/null && COPIED=true
|
|
1954
|
-
elif command -v pbcopy >/dev/null 2>&1; then
|
|
1955
|
-
printf "%s" "$MCP_URL" | pbcopy 2>/dev/null && COPIED=true
|
|
1956
|
-
elif command -v clip.exe >/dev/null 2>&1; then
|
|
1957
|
-
printf "%s" "$MCP_URL" | clip.exe 2>/dev/null && COPIED=true
|
|
1958
|
-
fi
|
|
1959
|
-
|
|
1960
|
-
clear 2>/dev/null || printf "\033[2J\033[H" || true
|
|
1961
|
-
|
|
1962
|
-
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1963
|
-
echo ""
|
|
1964
|
-
copy_msg=""
|
|
1965
|
-
if [ "$COPIED" = true ]; then
|
|
1966
|
-
copy_msg="[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)"
|
|
1967
|
-
else
|
|
1968
|
-
copy_msg="Скопируйте ссылку выше"
|
|
1969
|
-
fi
|
|
1970
|
-
print_mcp_box "ССЫЛКА MCP-СЕРВЕРА ДЛЯ ПОДКЛЮЧЕНИЯ (СКОПИРУЙТЕ):" "$MCP_URL" "$copy_msg"
|
|
1971
|
-
echo ""
|
|
1972
|
-
echo -e " ${BOLD}Куда вставлять ссылку в Google Gemini:${RESET}"
|
|
1973
|
-
echo -e " 1. Откройте в браузере: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
|
|
1974
|
-
echo -e " 2. Нажмите ${BOLD}'Добавить приложение'${RESET} (Add app / Настройки MCP)"
|
|
1975
|
-
echo -e " 3. Вставьте скопированную ссылку в поле ${BOLD}'URL сервера'${RESET} (${BOLD}Ctrl+V${RESET}) и нажмите ${BOLD}Подключить${RESET}."
|
|
1976
|
-
echo ""
|
|
1977
|
-
else
|
|
1978
|
-
echo ""
|
|
1979
|
-
copy_msg=""
|
|
1980
|
-
if [ "$COPIED" = true ]; then
|
|
1981
|
-
copy_msg="[OK] URL COPIED TO CLIPBOARD! (Paste with Ctrl+V)"
|
|
1982
|
-
else
|
|
1983
|
-
copy_msg="Copy the URL above"
|
|
1984
|
-
fi
|
|
1985
|
-
print_mcp_box "MCP SERVER CONNECTION URL (COPY THIS):" "$MCP_URL" "$copy_msg"
|
|
1986
|
-
echo ""
|
|
1987
|
-
echo -e " ${BOLD}Where to paste this URL in Google Gemini:${RESET}"
|
|
1988
|
-
echo -e " 1. Open in your browser: ${CYAN}https://gemini.google.com/spark/apps${RESET}"
|
|
1989
|
-
echo -e " 2. Click ${BOLD}'Add app'${RESET} (or navigate to MCP settings)"
|
|
1990
|
-
echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
|
|
1991
|
-
echo ""
|
|
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
|