jishushell 0.4.10 → 0.4.24
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/Dockerfile.hermes-slim +193 -0
- package/INSTALL-NOTICE +10 -12
- package/apps/hermes-container.yaml +35 -0
- package/apps/ollama-binary.yaml +200 -0
- package/apps/ollama-cpu-container.yaml +37 -0
- package/apps/ollama-with-hollama-binary.yaml +195 -0
- package/apps/openclaw-binary.yaml +69 -0
- package/apps/openclaw-container.yaml +37 -0
- package/apps/openclaw-with-ollama-container.yaml +42 -0
- package/apps/openclaw-with-searxng-container.yaml +136 -0
- package/apps/openwebui-container.yaml +53 -0
- package/apps/playwright-container.yaml +120 -0
- package/apps/searxng-container.yaml +115 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +15 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli/app.d.ts +4 -0
- package/dist/cli/app.js +814 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/backup.d.ts +3 -0
- package/dist/cli/backup.js +434 -0
- package/dist/cli/backup.js.map +1 -0
- package/dist/{doctor.d.ts → cli/doctor.d.ts} +7 -1
- package/dist/{doctor.js → cli/doctor.js} +377 -22
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/helpers.d.ts +4 -0
- package/dist/cli/helpers.js +32 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/job.d.ts +4 -0
- package/dist/cli/job.js +198 -0
- package/dist/cli/job.js.map +1 -0
- package/dist/cli/llm.d.ts +25 -0
- package/dist/cli/llm.js +599 -0
- package/dist/cli/llm.js.map +1 -0
- package/dist/cli/managed-list.d.ts +30 -0
- package/dist/cli/managed-list.js +129 -0
- package/dist/cli/managed-list.js.map +1 -0
- package/dist/cli/panel.d.ts +26 -0
- package/dist/cli/panel.js +804 -0
- package/dist/cli/panel.js.map +1 -0
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +12 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +48 -776
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +69 -0
- package/dist/config.js +268 -7
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +17 -41
- package/dist/control.js +61 -1323
- package/dist/control.js.map +1 -1
- package/dist/install.d.ts +16 -0
- package/dist/install.js +75 -26
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +15 -0
- package/dist/routes/agent-apps.js +78 -0
- package/dist/routes/agent-apps.js.map +1 -0
- package/dist/routes/apps.d.ts +3 -0
- package/dist/routes/apps.js +278 -0
- package/dist/routes/apps.js.map +1 -0
- package/dist/routes/backup.js +3 -3
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/instances.d.ts +6 -0
- package/dist/routes/instances.js +863 -874
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.d.ts +15 -0
- package/dist/routes/llm.js +247 -0
- package/dist/routes/llm.js.map +1 -0
- package/dist/routes/runtime.d.ts +15 -0
- package/dist/routes/runtime.js +69 -0
- package/dist/routes/runtime.js.map +1 -0
- package/dist/routes/setup.js +131 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +56 -9
- package/dist/routes/system.js.map +1 -1
- package/dist/server.js +107 -7
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +30 -0
- package/dist/services/agent-apps/catalog.js +60 -0
- package/dist/services/agent-apps/catalog.js.map +1 -0
- package/dist/services/agent-apps/index.d.ts +36 -0
- package/dist/services/agent-apps/index.js +171 -0
- package/dist/services/agent-apps/index.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter-probes.d.ts +49 -0
- package/dist/services/agent-apps/installers/adapter-probes.js +223 -0
- package/dist/services/agent-apps/installers/adapter-probes.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter.d.ts +30 -0
- package/dist/services/agent-apps/installers/adapter.js +171 -0
- package/dist/services/agent-apps/installers/adapter.js.map +1 -0
- package/dist/services/agent-apps/installers/registry-probe.d.ts +38 -0
- package/dist/services/agent-apps/installers/registry-probe.js +183 -0
- package/dist/services/agent-apps/installers/registry-probe.js.map +1 -0
- package/dist/services/agent-apps/installers/shell-script.d.ts +47 -0
- package/dist/services/agent-apps/installers/shell-script.js +471 -0
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -0
- package/dist/services/agent-apps/types.d.ts +125 -0
- package/dist/services/agent-apps/types.js +17 -0
- package/dist/services/agent-apps/types.js.map +1 -0
- package/dist/services/app/app-compiler.d.ts +15 -0
- package/dist/services/app/app-compiler.js +172 -0
- package/dist/services/app/app-compiler.js.map +1 -0
- package/dist/services/app/app-manager.d.ts +142 -0
- package/dist/services/app/app-manager.js +1988 -0
- package/dist/services/app/app-manager.js.map +1 -0
- package/dist/services/app/custom-manager.d.ts +27 -0
- package/dist/services/app/custom-manager.js +285 -0
- package/dist/services/app/custom-manager.js.map +1 -0
- package/dist/services/app/hermes-agent-manager.d.ts +20 -0
- package/dist/services/app/hermes-agent-manager.js +289 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -0
- package/dist/services/app/id-normalizer.d.ts +27 -0
- package/dist/services/app/id-normalizer.js +77 -0
- package/dist/services/app/id-normalizer.js.map +1 -0
- package/dist/services/app/ollama-manager.d.ts +18 -0
- package/dist/services/app/ollama-manager.js +207 -0
- package/dist/services/app/ollama-manager.js.map +1 -0
- package/dist/services/app/openclaw-manager.d.ts +63 -0
- package/dist/services/app/openclaw-manager.js +1178 -0
- package/dist/services/app/openclaw-manager.js.map +1 -0
- package/dist/services/app/paths.d.ts +47 -0
- package/dist/services/app/paths.js +68 -0
- package/dist/services/app/paths.js.map +1 -0
- package/dist/services/app/registry.d.ts +17 -0
- package/dist/services/app/registry.js +31 -0
- package/dist/services/app/registry.js.map +1 -0
- package/dist/services/app/remote-spec.d.ts +14 -0
- package/dist/services/app/remote-spec.js +58 -0
- package/dist/services/app/remote-spec.js.map +1 -0
- package/dist/services/app/terminal-session-manager.d.ts +27 -0
- package/dist/services/app/terminal-session-manager.js +157 -0
- package/dist/services/app/terminal-session-manager.js.map +1 -0
- package/dist/services/app/types.d.ts +72 -0
- package/dist/services/app/types.js +16 -0
- package/dist/services/app/types.js.map +1 -0
- package/dist/services/backup-manager.js +60 -22
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/instance-manager.d.ts +125 -34
- package/dist/services/instance-manager.js +679 -1043
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/adapters.js +5 -1
- package/dist/services/llm-proxy/adapters.js.map +1 -1
- package/dist/services/llm-proxy/circuit-breaker.js +10 -2
- package/dist/services/llm-proxy/circuit-breaker.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +43 -0
- package/dist/services/llm-proxy/index.js +120 -5
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +1 -1
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +260 -3
- package/dist/services/nomad-manager.js +2921 -341
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/panel-manager.d.ts +50 -0
- package/dist/services/panel-manager.js +443 -0
- package/dist/services/panel-manager.js.map +1 -0
- package/dist/services/plugin-installer.js +28 -2
- package/dist/services/plugin-installer.js.map +1 -1
- package/dist/services/process-manager.js +42 -7
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.d.ts +20 -0
- package/dist/services/runtime/adapters/custom.js +90 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -0
- package/dist/services/runtime/adapters/hermes.d.ts +174 -0
- package/dist/services/runtime/adapters/hermes.js +1316 -0
- package/dist/services/runtime/adapters/hermes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +17 -0
- package/dist/services/runtime/adapters/openclaw-routes.js +946 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw.d.ts +188 -0
- package/dist/services/runtime/adapters/openclaw.js +2195 -0
- package/dist/services/runtime/adapters/openclaw.js.map +1 -0
- package/dist/services/runtime/errors.d.ts +28 -0
- package/dist/services/runtime/errors.js +31 -0
- package/dist/services/runtime/errors.js.map +1 -0
- package/dist/services/runtime/index.d.ts +34 -0
- package/dist/services/runtime/index.js +51 -0
- package/dist/services/runtime/index.js.map +1 -0
- package/dist/services/runtime/instance.d.ts +24 -0
- package/dist/services/runtime/instance.js +143 -0
- package/dist/services/runtime/instance.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +15 -0
- package/dist/services/runtime/migrations.js +25 -0
- package/dist/services/runtime/migrations.js.map +1 -0
- package/dist/services/runtime/registry.d.ts +13 -0
- package/dist/services/runtime/registry.js +32 -0
- package/dist/services/runtime/registry.js.map +1 -0
- package/dist/services/runtime/types.d.ts +545 -0
- package/dist/services/runtime/types.js +14 -0
- package/dist/services/runtime/types.js.map +1 -0
- package/dist/services/setup-manager.d.ts +70 -29
- package/dist/services/setup-manager.js +591 -625
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/task-registry.d.ts +44 -0
- package/dist/services/task-registry.js +74 -0
- package/dist/services/task-registry.js.map +1 -0
- package/dist/services/telemetry/heartbeat.d.ts +6 -6
- package/dist/services/telemetry/heartbeat.js +29 -30
- package/dist/services/telemetry/heartbeat.js.map +1 -1
- package/dist/services/update-manager.d.ts +47 -0
- package/dist/services/update-manager.js +305 -0
- package/dist/services/update-manager.js.map +1 -0
- package/dist/types.d.ts +222 -0
- package/dist/utils/docker-host.d.ts +15 -0
- package/dist/utils/docker-host.js +64 -0
- package/dist/utils/docker-host.js.map +1 -0
- package/install/jishu-install.sh +303 -37
- package/install/post-install.sh +64 -5
- package/package.json +19 -5
- package/public/assets/Dashboard-B-JoOjBQ.js +1 -0
- package/public/assets/HermesChatPanel-mFSureyc.js +1 -0
- package/public/assets/HermesConfigForm-DvR05LK1.js +4 -0
- package/public/assets/InitPassword-CVA8wQA6.js +1 -0
- package/public/assets/InstanceDetail-DcZW2QGO.js +91 -0
- package/public/assets/{Login-CUoEZOWR.js → Login-BWsZH2mu.js} +1 -1
- package/public/assets/NewInstance-BCIrAd86.js +1 -0
- package/public/assets/Settings-xkDcduFz.js +1 -0
- package/public/assets/Setup-Cfuwj4gV.js +1 -0
- package/public/assets/WeixinLoginPanel-CnjR8xMu.js +9 -0
- package/public/assets/index-CPhVFEsx.css +1 -0
- package/public/assets/index-DQsM6Joa.js +19 -0
- package/public/assets/input-paste-CrNVAyOy.js +1 -0
- package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
- package/public/assets/registry-B4UFJdpA.js +2 -0
- package/public/assets/{usePolling-CK0DfI4h.js → usePolling-Do5Erqm_.js} +1 -1
- package/public/assets/vendor-i18n-ucpM0OR0.js +9 -0
- package/public/assets/{vendor-react-B1-3Yrt-.js → vendor-react-Bk1hRGiY.js} +1 -1
- package/public/favicon.png +0 -0
- package/public/index.html +9 -4
- package/public/logos/hermes.png +0 -0
- package/public/logos/ollama.png +0 -0
- package/public/logos/openclaw.svg +60 -0
- package/scripts/build-hermes-image.sh +21 -0
- package/scripts/build-local.sh +54 -0
- package/scripts/check-adapter-isolation.ts +293 -0
- package/scripts/fixtures/instances/hermes-sample/instance.json +37 -0
- package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +7 -0
- package/scripts/smoke/hermes-bootstrap.sh +195 -0
- package/templates/hermes-entrypoint.sh +154 -0
- package/dist/doctor.js.map +0 -1
- package/install/jishu-install-china.sh +0 -3092
- package/public/assets/Dashboard-DhsrzJ4F.js +0 -1
- package/public/assets/InitPassword-BjubiVdd.js +0 -1
- package/public/assets/InstanceDetail-DMcywsof.js +0 -17
- package/public/assets/NewInstance-Bk0G4EiJ.js +0 -1
- package/public/assets/Settings-D5tHL_h5.js +0 -1
- package/public/assets/Setup-4t6E3Rut.js +0 -1
- package/public/assets/index-BJ47MWpF.css +0 -1
- package/public/assets/index-DbX85irc.js +0 -16
- package/public/assets/vendor-i18n-CfW0RvgE.js +0 -9
|
@@ -1,3092 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
-
# JishuShell Installer
|
|
6
|
-
#
|
|
7
|
-
# Self-contained installer for all JishuShell dependencies:
|
|
8
|
-
# Node.js (via nvm), Docker, Nomad, OpenClaw Docker image
|
|
9
|
-
#
|
|
10
|
-
# Can also be sourced by other scripts (post-install.sh, jishu-uninstall.sh)
|
|
11
|
-
# to reuse shared functions — main() only runs when executed directly.
|
|
12
|
-
#
|
|
13
|
-
# Usage:
|
|
14
|
-
# bash jishu-install.sh [options] — run the full installer
|
|
15
|
-
# source jishu-install.sh — import functions only
|
|
16
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
-
|
|
18
|
-
# Guard against double-sourcing
|
|
19
|
-
if [[ -n "${_JISHU_INSTALL_LOADED:-}" ]]; then
|
|
20
|
-
return 0 2>/dev/null || true
|
|
21
|
-
fi
|
|
22
|
-
_JISHU_INSTALL_LOADED=1
|
|
23
|
-
|
|
24
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
25
|
-
# Temporary File Management (trap cleanup)
|
|
26
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
27
|
-
TMPFILES=()
|
|
28
|
-
_SUDO_KEEPALIVE_PID=""
|
|
29
|
-
|
|
30
|
-
cleanup_tmpfiles() {
|
|
31
|
-
local f
|
|
32
|
-
for f in "${TMPFILES[@]:-}"; do
|
|
33
|
-
rm -rf "$f" 2>/dev/null || true
|
|
34
|
-
done
|
|
35
|
-
# Kill sudo keepalive background process if running
|
|
36
|
-
if [[ -n "${_SUDO_KEEPALIVE_PID:-}" ]]; then
|
|
37
|
-
kill "$_SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
|
38
|
-
fi
|
|
39
|
-
}
|
|
40
|
-
trap cleanup_tmpfiles EXIT
|
|
41
|
-
|
|
42
|
-
mktempfile() {
|
|
43
|
-
local f
|
|
44
|
-
f="$(mktemp)"
|
|
45
|
-
TMPFILES+=("$f")
|
|
46
|
-
echo "$f"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
50
|
-
# Download Tool Detection
|
|
51
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
52
|
-
DOWNLOADER=""
|
|
53
|
-
detect_downloader() {
|
|
54
|
-
if command -v curl &> /dev/null; then
|
|
55
|
-
DOWNLOADER="curl"
|
|
56
|
-
return 0
|
|
57
|
-
fi
|
|
58
|
-
if command -v wget &> /dev/null; then
|
|
59
|
-
DOWNLOADER="wget"
|
|
60
|
-
return 0
|
|
61
|
-
fi
|
|
62
|
-
ui_error "Missing downloader (curl or wget required)"
|
|
63
|
-
exit 1
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
download_file() {
|
|
67
|
-
local url="$1"
|
|
68
|
-
local output="$2"
|
|
69
|
-
if [[ -z "$DOWNLOADER" ]]; then
|
|
70
|
-
detect_downloader
|
|
71
|
-
fi
|
|
72
|
-
if [[ "$DOWNLOADER" == "curl" ]]; then
|
|
73
|
-
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
|
|
74
|
-
return
|
|
75
|
-
fi
|
|
76
|
-
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
# retry_net <description> <max_attempts> <cmd> [args...]
|
|
80
|
-
# Runs cmd up to max_attempts times; waits 5s, 10s between retries.
|
|
81
|
-
# Network-related exit codes (6,7,28,35,56 for curl; DNS/timeout) are all retried.
|
|
82
|
-
retry_net() {
|
|
83
|
-
local desc="$1"
|
|
84
|
-
local max="${2:-3}"
|
|
85
|
-
shift 2
|
|
86
|
-
local attempt=1
|
|
87
|
-
local delay=5
|
|
88
|
-
while true; do
|
|
89
|
-
if "$@"; then
|
|
90
|
-
return 0
|
|
91
|
-
fi
|
|
92
|
-
local rc=$?
|
|
93
|
-
if [[ $attempt -ge $max ]]; then
|
|
94
|
-
ui_error "${desc} failed after ${max} attempts (last exit code: ${rc})"
|
|
95
|
-
return $rc
|
|
96
|
-
fi
|
|
97
|
-
ui_warn "${desc} failed (attempt ${attempt}/${max}, exit ${rc}) — retrying in ${delay}s..."
|
|
98
|
-
sleep "$delay"
|
|
99
|
-
attempt=$(( attempt + 1 ))
|
|
100
|
-
delay=$(( delay * 2 ))
|
|
101
|
-
done
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
105
|
-
# Non-interactive Shell Detection
|
|
106
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
107
|
-
is_non_interactive_shell() {
|
|
108
|
-
if [[ "${NO_PROMPT:-0}" == "1" ]]; then
|
|
109
|
-
return 0
|
|
110
|
-
fi
|
|
111
|
-
if [[ ! -t 0 || ! -t 1 ]]; then
|
|
112
|
-
return 0
|
|
113
|
-
fi
|
|
114
|
-
return 1
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
is_promptable() {
|
|
118
|
-
if [[ "${NO_PROMPT:-0}" == "1" ]]; then
|
|
119
|
-
return 1
|
|
120
|
-
fi
|
|
121
|
-
if [[ -r /dev/tty && -w /dev/tty ]]; then
|
|
122
|
-
return 0
|
|
123
|
-
fi
|
|
124
|
-
return 1
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
128
|
-
# Holiday Taglines (fun Easter eggs)
|
|
129
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
130
|
-
HOLIDAY_NEW_YEAR="New year, new config—same old EADDRINUSE, but this time we resolve it like grown-ups."
|
|
131
|
-
HOLIDAY_CHRISTMAS="Ho ho ho—Santa's little helper is here to ship joy, roll back chaos, and stash the keys safely."
|
|
132
|
-
HOLIDAY_HALLOWEEN="Spooky season: beware haunted dependencies, cursed caches, and the ghost of node_modules past."
|
|
133
|
-
HOLIDAY_THANKSGIVING="Grateful for stable ports, working DNS, and a bot that reads the logs so nobody has to."
|
|
134
|
-
|
|
135
|
-
append_holiday_taglines() {
|
|
136
|
-
local month_day
|
|
137
|
-
month_day="$(date -u +%m-%d 2>/dev/null || date +%m-%d)"
|
|
138
|
-
case "$month_day" in
|
|
139
|
-
"01-01") TAGLINE="${HOLIDAY_NEW_YEAR}" ;;
|
|
140
|
-
"12-25") TAGLINE="${HOLIDAY_CHRISTMAS}" ;;
|
|
141
|
-
"10-31") TAGLINE="${HOLIDAY_HALLOWEEN}" ;;
|
|
142
|
-
"11-27"|"11-28") TAGLINE="${HOLIDAY_THANKSGIVING}" ;;
|
|
143
|
-
esac
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
pick_tagline() {
|
|
147
|
-
if [[ -n "${JISHU_TAGLINE:-}" ]]; then
|
|
148
|
-
TAGLINE="${JISHU_TAGLINE}"
|
|
149
|
-
return
|
|
150
|
-
fi
|
|
151
|
-
append_holiday_taglines
|
|
152
|
-
if [[ -z "${TAGLINE:-}" ]]; then
|
|
153
|
-
TAGLINE=" All your agents, one JishuShell."
|
|
154
|
-
fi
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Script directory (BASH_SOURCE[0] is unset when piped via curl|bash, fall back to $0)
|
|
158
|
-
JISHU_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd 2>/dev/null || pwd)"
|
|
159
|
-
|
|
160
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
-
# ──── BEGIN VERSIONS ────
|
|
162
|
-
NODE_VERSION="${JISHU_NODE_VERSION:-22}"
|
|
163
|
-
NVM_VERSION="${JISHU_NVM_VERSION:-0.40.4}"
|
|
164
|
-
NOMAD_VERSION="${JISHU_NOMAD_VERSION:-1.11.3}"
|
|
165
|
-
JISHUSHELL_PORT="${JISHUSHELL_PORT:-8090}"
|
|
166
|
-
|
|
167
|
-
# ──── NPM Registry Configuration ────
|
|
168
|
-
# Pass --registry <url> to override the npm registry for all installs.
|
|
169
|
-
NPM_REGISTRY="${NPM_REGISTRY:-}"
|
|
170
|
-
# Resolve the real (non-root) user when running via sudo.
|
|
171
|
-
# REAL_USER / REAL_HOME are used wherever paths must belong to the target user.
|
|
172
|
-
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
|
|
173
|
-
REAL_USER="${SUDO_USER}"
|
|
174
|
-
REAL_HOME="$(getent passwd "${SUDO_USER}" | cut -d: -f6 2>/dev/null || echo "")"
|
|
175
|
-
elif [[ $EUID -eq 0 ]]; then
|
|
176
|
-
# Running as root without sudo — try to detect the first non-root UID-1000 user
|
|
177
|
-
# (common default user on Raspberry Pi, Ubuntu, etc.)
|
|
178
|
-
_fallback_user="$(getent passwd 1000 2>/dev/null | cut -d: -f1 || true)"
|
|
179
|
-
if [[ -n "${_fallback_user}" && "${_fallback_user}" != "root" ]]; then
|
|
180
|
-
REAL_USER="${_fallback_user}"
|
|
181
|
-
REAL_HOME="$(getent passwd "${_fallback_user}" | cut -d: -f6 2>/dev/null || echo "")"
|
|
182
|
-
else
|
|
183
|
-
REAL_USER="$(id -un)"
|
|
184
|
-
REAL_HOME="${HOME}"
|
|
185
|
-
fi
|
|
186
|
-
unset _fallback_user
|
|
187
|
-
else
|
|
188
|
-
REAL_USER="$(id -un)"
|
|
189
|
-
REAL_HOME="${HOME}"
|
|
190
|
-
fi
|
|
191
|
-
[[ -z "${REAL_HOME}" ]] && REAL_HOME="${HOME}"
|
|
192
|
-
REAL_GID="$(id -g "${REAL_USER}" 2>/dev/null || echo "")"
|
|
193
|
-
|
|
194
|
-
# JISHUSHELL_HOME always lives in the real user's home so data is accessible
|
|
195
|
-
# regardless of whether the process runs as root or as that user.
|
|
196
|
-
if [[ -z "${JISHUSHELL_HOME:-}" ]]; then
|
|
197
|
-
JISHUSHELL_HOME="${REAL_HOME}/.jishushell"
|
|
198
|
-
fi
|
|
199
|
-
JISHUSHELL_BIN_DIR="${JISHUSHELL_BIN_DIR:-$JISHUSHELL_HOME/bin}"
|
|
200
|
-
# ──── END VERSIONS ────
|
|
201
|
-
|
|
202
|
-
# Pick tagline
|
|
203
|
-
TAGLINE=""
|
|
204
|
-
pick_tagline
|
|
205
|
-
|
|
206
|
-
# Colors
|
|
207
|
-
BOLD='\033[1m'
|
|
208
|
-
ACCENT='\033[38;2;66;135;245m'
|
|
209
|
-
ACCENT_CORAL='\033[38;2;255;77;77m'
|
|
210
|
-
INFO='\033[38;2;136;146;176m'
|
|
211
|
-
SUCCESS='\033[38;2;0;229;204m'
|
|
212
|
-
WARN='\033[38;2;255;176;32m'
|
|
213
|
-
ERROR='\033[38;2;230;57;70m'
|
|
214
|
-
MUTED='\033[38;2;90;100;128m'
|
|
215
|
-
NC='\033[0m'
|
|
216
|
-
|
|
217
|
-
# Shared state
|
|
218
|
-
VERBOSE="${VERBOSE:-0}"
|
|
219
|
-
DRY_RUN="${DRY_RUN:-0}"
|
|
220
|
-
SKIP_NODE="${SKIP_NODE:-0}"
|
|
221
|
-
SKIP_DOCKER="${SKIP_DOCKER:-0}"
|
|
222
|
-
SKIP_NOMAD="${SKIP_NOMAD:-0}"
|
|
223
|
-
SKIP_OPENCLAW="${SKIP_OPENCLAW:-0}" # default=0 (install); use --skip 4 or --skip-openclaw to skip
|
|
224
|
-
SKIP_JISHUSHELL="${SKIP_JISHUSHELL:-0}" # 1=skip install_jishushell
|
|
225
|
-
SKIP_JISHUSHELL_SERVICE="${SKIP_JISHUSHELL_SERVICE:-0}" # 1=skip service registration
|
|
226
|
-
OPENCLAW_NPM_VERSION="${OPENCLAW_NPM_VERSION:-latest}" # openclaw npm package version
|
|
227
|
-
OPENCLAW_DOCKER_TAG="${OPENCLAW_DOCKER_TAG:-jishushell-openclaw:local}" # locally built image with Python
|
|
228
|
-
OPENCLAW_IMAGE="" # set dynamically after pull/build
|
|
229
|
-
AUTO_YES="${AUTO_YES:-0}"
|
|
230
|
-
DOCKER_CMD_PREFIX="" # Set to "sg docker -c" when group activated via sg
|
|
231
|
-
DOCKER_GROUP_JUST_ADDED=0 # 1 if usermod was called this run
|
|
232
|
-
DOCKER_USE_SUDO=0 # 1 if sudo docker should be used as fallback
|
|
233
|
-
NETWORK_CHINA=0 # 1 if www.google.com is unreachable (use Alibaba mirrors)
|
|
234
|
-
NETWORK_OVERRIDE=0 # 1 if --network was explicitly passed (skip auto-detection)
|
|
235
|
-
|
|
236
|
-
PKG_UPDATED=0
|
|
237
|
-
|
|
238
|
-
INSTALL_STAGE_TOTAL="${INSTALL_STAGE_TOTAL:-6}"
|
|
239
|
-
INSTALL_STAGE_CURRENT="${INSTALL_STAGE_CURRENT:-0}"
|
|
240
|
-
JISHU_LOG_FILE=""
|
|
241
|
-
_JISHU_RAW_LOG=""
|
|
242
|
-
_JISHU_TEE_PID=""
|
|
243
|
-
_JISHU_LOG_FIFO=""
|
|
244
|
-
_JISHU_DETAIL_LOG=""
|
|
245
|
-
|
|
246
|
-
# Legacy environment variable mapping
|
|
247
|
-
map_legacy_env() {
|
|
248
|
-
local key="$1"
|
|
249
|
-
local legacy="$2"
|
|
250
|
-
if [[ -z "${!key:-}" && -n "${!legacy:-}" ]]; then
|
|
251
|
-
printf -v "$key" '%s' "${!legacy}"
|
|
252
|
-
fi
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
# (legacy env var mappings reserved for future renames)
|
|
256
|
-
|
|
257
|
-
# ─── UI helpers ───────────────────────────────────────────────────────────────
|
|
258
|
-
ui_info() {
|
|
259
|
-
echo -e "${MUTED}·${NC} $*"
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
ui_success() {
|
|
263
|
-
echo -e "${SUCCESS}✓${NC} $*"
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
ui_warn() {
|
|
267
|
-
echo -e "${WARN}!${NC} $*"
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
ui_error() {
|
|
271
|
-
echo -e "${ERROR}✗${NC} $*" >&2
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
ui_stage() {
|
|
275
|
-
local title="$1"
|
|
276
|
-
INSTALL_STAGE_CURRENT=$((INSTALL_STAGE_CURRENT + 1))
|
|
277
|
-
echo ""
|
|
278
|
-
echo -e "${ACCENT}${BOLD}[${INSTALL_STAGE_CURRENT}/${INSTALL_STAGE_TOTAL}] ${title}${NC}"
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
ui_section() {
|
|
282
|
-
echo -e "\n${ACCENT}${BOLD}── $* ──${NC}"
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
ui_kv() {
|
|
286
|
-
local key="$1" value="$2"
|
|
287
|
-
printf "${MUTED}%-18s${NC} %s\n" "$key:" "$value"
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
# ─── Detail-log helpers ───────────────────────────────────────────────────────
|
|
291
|
-
# Appends one line directly to the detail log file (bypasses the terminal pipe).
|
|
292
|
-
log_detail() {
|
|
293
|
-
[[ -n "${_JISHU_DETAIL_LOG:-}" ]] || return 0
|
|
294
|
-
printf '%s\n' "$*" >> "${_JISHU_DETAIL_LOG}"
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
# Runs a command, capturing stdout+stderr to the detail log file only.
|
|
298
|
-
# Nothing is written to the terminal — use ui_* helpers for status messages.
|
|
299
|
-
# Returns the command's exit code.
|
|
300
|
-
log_cmd() {
|
|
301
|
-
local _rc=0
|
|
302
|
-
if [[ -z "${_JISHU_DETAIL_LOG:-}" ]]; then
|
|
303
|
-
"$@" 2>&1
|
|
304
|
-
return
|
|
305
|
-
fi
|
|
306
|
-
log_detail ""
|
|
307
|
-
log_detail "[$(date '+%H:%M:%S')] \$ $*"
|
|
308
|
-
# Capture output to detail log only (not forwarded to the terminal)
|
|
309
|
-
local _tmp
|
|
310
|
-
_tmp="$(mktemp)"
|
|
311
|
-
"$@" > "${_tmp}" 2>&1 || _rc=$?
|
|
312
|
-
if [[ -s "${_tmp}" ]]; then
|
|
313
|
-
sed 's/^/ /' "${_tmp}" >> "${_JISHU_DETAIL_LOG}" # → detail log only
|
|
314
|
-
fi
|
|
315
|
-
rm -f "${_tmp}"
|
|
316
|
-
log_detail " \u2192 exit ${_rc}"
|
|
317
|
-
return $_rc
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
# Like log_cmd but prepends \$SUDO when set (mirrors run_sudo behaviour).
|
|
321
|
-
_log_sudo() {
|
|
322
|
-
if [[ -n "${SUDO:-}" ]]; then
|
|
323
|
-
log_cmd "${SUDO}" "$@"
|
|
324
|
-
else
|
|
325
|
-
log_cmd "$@"
|
|
326
|
-
fi
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
# ─── User confirmation prompt ────────────────────────────────────────────────
|
|
330
|
-
# Usage: confirm "Delete everything?" && do_delete
|
|
331
|
-
# Respects AUTO_YES=1 to skip prompts.
|
|
332
|
-
confirm() {
|
|
333
|
-
local prompt="$1"
|
|
334
|
-
if [[ "${AUTO_YES:-0}" == "1" ]]; then
|
|
335
|
-
ui_info "$prompt → auto-confirmed (--yes)"
|
|
336
|
-
return 0
|
|
337
|
-
fi
|
|
338
|
-
local answer answer_lc
|
|
339
|
-
read -r -p "$(echo -e "${WARN} ${prompt} [y/N]: ${NC}")" answer </dev/tty || answer="n"
|
|
340
|
-
answer_lc="$(echo "$answer" | tr '[:upper:]' '[:lower:]')"
|
|
341
|
-
case "$answer_lc" in
|
|
342
|
-
y|yes) return 0 ;;
|
|
343
|
-
*) return 1 ;;
|
|
344
|
-
esac
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
# ─── Utility functions ────────────────────────────────────────────────────────
|
|
348
|
-
|
|
349
|
-
# Detect OS and package manager.
|
|
350
|
-
# Sets: OS_ID, OS_VERSION, OS_NAME, PKG_MANAGER
|
|
351
|
-
detect_os() {
|
|
352
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
353
|
-
OS="macos"
|
|
354
|
-
OS_ID="macos"
|
|
355
|
-
OS_VERSION="$(sw_vers -productVersion 2>/dev/null || echo "unknown")"
|
|
356
|
-
OS_NAME="macOS ${OS_VERSION}"
|
|
357
|
-
if command -v brew &>/dev/null; then
|
|
358
|
-
PKG_MANAGER="brew"
|
|
359
|
-
else
|
|
360
|
-
PKG_MANAGER="none"
|
|
361
|
-
ui_warn "Homebrew not found — some optional installs may be skipped"
|
|
362
|
-
fi
|
|
363
|
-
ui_success "OS: ${OS_NAME} (package manager: ${PKG_MANAGER})"
|
|
364
|
-
return 0
|
|
365
|
-
fi
|
|
366
|
-
|
|
367
|
-
if [[ ! -f /etc/os-release ]]; then
|
|
368
|
-
ui_error "Cannot detect OS: /etc/os-release not found"
|
|
369
|
-
ui_error "This installer supports Linux and macOS"
|
|
370
|
-
exit 1
|
|
371
|
-
fi
|
|
372
|
-
|
|
373
|
-
# shellcheck source=/dev/null
|
|
374
|
-
. /etc/os-release
|
|
375
|
-
|
|
376
|
-
OS_ID="${ID:-unknown}"
|
|
377
|
-
OS_VERSION="${VERSION_ID:-unknown}"
|
|
378
|
-
OS_NAME="${PRETTY_NAME:-$OS_ID $OS_VERSION}"
|
|
379
|
-
|
|
380
|
-
case "$OS_ID" in
|
|
381
|
-
ubuntu|debian|linuxmint|pop)
|
|
382
|
-
PKG_MANAGER="apt"
|
|
383
|
-
;;
|
|
384
|
-
centos|rhel|rocky|almalinux|fedora|amzn)
|
|
385
|
-
if command -v dnf &>/dev/null; then
|
|
386
|
-
PKG_MANAGER="dnf"
|
|
387
|
-
else
|
|
388
|
-
PKG_MANAGER="yum"
|
|
389
|
-
fi
|
|
390
|
-
;;
|
|
391
|
-
*)
|
|
392
|
-
ui_warn "Untested distribution: $OS_ID — attempting auto-detect"
|
|
393
|
-
if command -v apt-get &>/dev/null; then
|
|
394
|
-
PKG_MANAGER="apt"
|
|
395
|
-
elif command -v dnf &>/dev/null; then
|
|
396
|
-
PKG_MANAGER="dnf"
|
|
397
|
-
elif command -v yum &>/dev/null; then
|
|
398
|
-
PKG_MANAGER="yum"
|
|
399
|
-
else
|
|
400
|
-
ui_error "No supported package manager found (apt/dnf/yum)"
|
|
401
|
-
exit 1
|
|
402
|
-
fi
|
|
403
|
-
;;
|
|
404
|
-
esac
|
|
405
|
-
|
|
406
|
-
OS="linux"
|
|
407
|
-
ui_success "OS: ${OS_NAME} (package manager: ${PKG_MANAGER})"
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
# Detect CPU architecture. Sets: ARCH (amd64 | arm64)
|
|
411
|
-
detect_arch() {
|
|
412
|
-
ARCH="$(uname -m)"
|
|
413
|
-
case "$ARCH" in
|
|
414
|
-
x86_64|amd64) ARCH="amd64" ;;
|
|
415
|
-
aarch64|arm64) ARCH="arm64" ;;
|
|
416
|
-
*)
|
|
417
|
-
ui_error "Unsupported CPU architecture: $ARCH"
|
|
418
|
-
exit 1
|
|
419
|
-
;;
|
|
420
|
-
esac
|
|
421
|
-
ui_success "Architecture: ${ARCH}"
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
# Detect network environment by pinging www.google.com. Sets: NETWORK_CHINA (0=global, 1=China)
|
|
425
|
-
detect_network() {
|
|
426
|
-
if [[ "${NETWORK_OVERRIDE:-0}" == "1" ]]; then
|
|
427
|
-
if [[ "$NETWORK_CHINA" == "1" ]]; then
|
|
428
|
-
ui_info "Network: forced china (--network china) — Alibaba mirrors will be used"
|
|
429
|
-
else
|
|
430
|
-
ui_info "Network: forced global (--network global)"
|
|
431
|
-
fi
|
|
432
|
-
return 0
|
|
433
|
-
fi
|
|
434
|
-
ui_info "Checking network connectivity (ping www.google.com)..."
|
|
435
|
-
if ping -c 1 -W 3 www.google.com &>/dev/null 2>&1; then
|
|
436
|
-
NETWORK_CHINA=0
|
|
437
|
-
ui_success "Network: global reachable"
|
|
438
|
-
else
|
|
439
|
-
NETWORK_CHINA=1
|
|
440
|
-
ui_warn "Network: www.google.com unreachable — Alibaba mirrors will be used for Docker and OpenClaw"
|
|
441
|
-
fi
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
# Verify sudo access. Sets: SUDO ("" if root, "sudo" otherwise)
|
|
445
|
-
check_sudo() {
|
|
446
|
-
if [[ $EUID -eq 0 ]]; then
|
|
447
|
-
SUDO=""
|
|
448
|
-
return 0
|
|
449
|
-
fi
|
|
450
|
-
|
|
451
|
-
if ! command -v sudo &>/dev/null; then
|
|
452
|
-
ui_error "Not running as root and sudo is not installed. Re-run as root."
|
|
453
|
-
exit 1
|
|
454
|
-
fi
|
|
455
|
-
|
|
456
|
-
if ! sudo -n true 2>/dev/null; then
|
|
457
|
-
ui_info "Some steps require sudo — you may be prompted for your password."
|
|
458
|
-
if ! sudo -v </dev/tty; then
|
|
459
|
-
ui_error "Failed to obtain sudo privileges"
|
|
460
|
-
exit 1
|
|
461
|
-
fi
|
|
462
|
-
fi
|
|
463
|
-
|
|
464
|
-
SUDO="sudo"
|
|
465
|
-
ui_success "sudo privileges confirmed"
|
|
466
|
-
|
|
467
|
-
# Keep sudo credentials alive for the entire install (refreshes every 60s).
|
|
468
|
-
# This ensures 'sudo docker' works even after a long Docker install step
|
|
469
|
-
# without prompting for the password again.
|
|
470
|
-
if [[ -z "${_SUDO_KEEPALIVE_PID:-}" ]]; then
|
|
471
|
-
( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &
|
|
472
|
-
_SUDO_KEEPALIVE_PID=$!
|
|
473
|
-
disown "$_SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
|
474
|
-
fi
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
extract_semver() {
|
|
478
|
-
grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
# Semantic version comparison: returns 0 if $1 >= $2
|
|
482
|
-
version_gte() {
|
|
483
|
-
local v1="$1" v2="$2"
|
|
484
|
-
if [[ "$(printf '%s\n%s' "$v1" "$v2" | sort -V | head -n1)" == "$v2" ]]; then
|
|
485
|
-
return 0
|
|
486
|
-
fi
|
|
487
|
-
return 1
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
# Run a command, honouring --dry-run.
|
|
491
|
-
# When the detail log is active, command output is captured there (silent on terminal).
|
|
492
|
-
run_cmd() {
|
|
493
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
494
|
-
ui_info "[dry-run] $*"
|
|
495
|
-
return 0
|
|
496
|
-
fi
|
|
497
|
-
log_detail ""
|
|
498
|
-
log_detail "[$(date '+%H:%M:%S')] \$ $*"
|
|
499
|
-
if [[ -n "${_JISHU_DETAIL_LOG:-}" ]]; then
|
|
500
|
-
local _rc=0 _tmp
|
|
501
|
-
_tmp="$(mktemp)"
|
|
502
|
-
"$@" > "${_tmp}" 2>&1 || _rc=$?
|
|
503
|
-
[[ -s "${_tmp}" ]] && sed 's/^/ /' "${_tmp}" >> "${_JISHU_DETAIL_LOG}"
|
|
504
|
-
rm -f "${_tmp}"
|
|
505
|
-
log_detail " \u2192 exit ${_rc}"
|
|
506
|
-
return $_rc
|
|
507
|
-
fi
|
|
508
|
-
"$@"
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
# Run a command with sudo, honouring --dry-run.
|
|
512
|
-
# When the detail log is active, command output is captured there (silent on terminal).
|
|
513
|
-
run_sudo() {
|
|
514
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
515
|
-
ui_info "[dry-run] ${SUDO:+sudo }$*"
|
|
516
|
-
return 0
|
|
517
|
-
fi
|
|
518
|
-
log_detail ""
|
|
519
|
-
log_detail "[$(date '+%H:%M:%S')] \$ ${SUDO:+${SUDO} }$*"
|
|
520
|
-
if [[ -n "${_JISHU_DETAIL_LOG:-}" ]]; then
|
|
521
|
-
local _rc=0 _tmp
|
|
522
|
-
_tmp="$(mktemp)"
|
|
523
|
-
${SUDO} "$@" > "${_tmp}" 2>&1 || _rc=$?
|
|
524
|
-
[[ -s "${_tmp}" ]] && sed 's/^/ /' "${_tmp}" >> "${_JISHU_DETAIL_LOG}"
|
|
525
|
-
rm -f "${_tmp}"
|
|
526
|
-
log_detail " \u2192 exit ${_rc}"
|
|
527
|
-
return $_rc
|
|
528
|
-
fi
|
|
529
|
-
${SUDO} "$@"
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
# Wait for apt/dpkg locks to be released
|
|
533
|
-
wait_for_apt_lock() {
|
|
534
|
-
if [[ "${PKG_MANAGER:-}" != "apt" ]]; then
|
|
535
|
-
return 0
|
|
536
|
-
fi
|
|
537
|
-
|
|
538
|
-
local max_wait=60
|
|
539
|
-
local waited=0
|
|
540
|
-
|
|
541
|
-
while fuser /var/lib/dpkg/lock-frontend &>/dev/null 2>&1 || \
|
|
542
|
-
fuser /var/lib/apt/lists/lock &>/dev/null 2>&1; do
|
|
543
|
-
if [[ $waited -eq 0 ]]; then
|
|
544
|
-
ui_info "Waiting for apt lock to be released..."
|
|
545
|
-
fi
|
|
546
|
-
sleep 2
|
|
547
|
-
waited=$((waited + 2))
|
|
548
|
-
if [[ $waited -ge $max_wait ]]; then
|
|
549
|
-
ui_error "Timed out waiting for apt lock (${max_wait}s). Check for other running package managers."
|
|
550
|
-
exit 1
|
|
551
|
-
fi
|
|
552
|
-
done
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
# Run the package index update once per invocation.
|
|
556
|
-
# Safe to call multiple times — only executes on the first call.
|
|
557
|
-
pkg_update() {
|
|
558
|
-
if [[ "$PKG_UPDATED" == "1" ]]; then
|
|
559
|
-
return 0
|
|
560
|
-
fi
|
|
561
|
-
|
|
562
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
563
|
-
ui_info "[dry-run] Would update package index"
|
|
564
|
-
PKG_UPDATED=1
|
|
565
|
-
return 0
|
|
566
|
-
fi
|
|
567
|
-
|
|
568
|
-
wait_for_apt_lock
|
|
569
|
-
|
|
570
|
-
ui_info "Updating package index..."
|
|
571
|
-
case "$PKG_MANAGER" in
|
|
572
|
-
apt)
|
|
573
|
-
if ! retry_net "apt-get update" 3 _log_sudo apt-get update; then
|
|
574
|
-
ui_warn "apt-get update failed — package index may be stale, continuing anyway"
|
|
575
|
-
fi
|
|
576
|
-
;;
|
|
577
|
-
dnf)
|
|
578
|
-
retry_net "dnf check-update" 3 _log_sudo dnf check-update 2>/dev/null || true
|
|
579
|
-
;;
|
|
580
|
-
yum)
|
|
581
|
-
retry_net "yum check-update" 3 _log_sudo yum check-update 2>/dev/null || true
|
|
582
|
-
;;
|
|
583
|
-
brew)
|
|
584
|
-
retry_net "brew update" 3 log_cmd brew update 2>/dev/null || true
|
|
585
|
-
;;
|
|
586
|
-
none)
|
|
587
|
-
;;
|
|
588
|
-
esac
|
|
589
|
-
|
|
590
|
-
PKG_UPDATED=1
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
# Install system packages, ensuring the index is up to date first.
|
|
594
|
-
pkg_install() {
|
|
595
|
-
pkg_update
|
|
596
|
-
wait_for_apt_lock
|
|
597
|
-
case "$PKG_MANAGER" in
|
|
598
|
-
apt)
|
|
599
|
-
if ! retry_net "apt-get install $*" 3 run_sudo apt-get install -y "$@"; then
|
|
600
|
-
ui_error "apt-get install failed for: $*"
|
|
601
|
-
return 1
|
|
602
|
-
fi
|
|
603
|
-
;;
|
|
604
|
-
dnf)
|
|
605
|
-
if ! retry_net "dnf install $*" 3 run_sudo dnf install -y "$@"; then
|
|
606
|
-
ui_error "dnf install failed for: $*"
|
|
607
|
-
return 1
|
|
608
|
-
fi
|
|
609
|
-
;;
|
|
610
|
-
yum)
|
|
611
|
-
if ! retry_net "yum install $*" 3 run_sudo yum install -y "$@"; then
|
|
612
|
-
ui_error "yum install failed for: $*"
|
|
613
|
-
return 1
|
|
614
|
-
fi
|
|
615
|
-
;;
|
|
616
|
-
brew)
|
|
617
|
-
if ! retry_net "brew install $*" 3 brew install "$@" 2>/dev/null; then
|
|
618
|
-
ui_warn "brew install failed for: $* (may already be installed)"
|
|
619
|
-
fi
|
|
620
|
-
;;
|
|
621
|
-
none)
|
|
622
|
-
ui_warn "No package manager available — skipping install of: $*"
|
|
623
|
-
;;
|
|
624
|
-
esac
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
# Check whether a system package is installed
|
|
628
|
-
_pkg_is_installed() {
|
|
629
|
-
local pkg="$1"
|
|
630
|
-
case "$PKG_MANAGER" in
|
|
631
|
-
apt) dpkg -l "$pkg" 2>/dev/null | grep -q '^ii' ;;
|
|
632
|
-
dnf) rpm -q "$pkg" &>/dev/null ;;
|
|
633
|
-
yum) rpm -q "$pkg" &>/dev/null ;;
|
|
634
|
-
esac
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
638
|
-
# npm install failure detection and auto-fix
|
|
639
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
640
|
-
npm_log_indicates_missing_build_tools() {
|
|
641
|
-
local log="$1"
|
|
642
|
-
if [[ -z "$log" || ! -f "$log" ]]; then
|
|
643
|
-
return 1
|
|
644
|
-
fi
|
|
645
|
-
grep -Eiq "(not found: make|make: command not found|cmake: command not found|CMAKE_MAKE_PROGRAM is not set|Could not find CMAKE|gyp ERR! find Python|no developer tools were found|is not able to compile a simple test program|Failed to build|It seems that \"make\" is not installed|It seems that the used \"cmake\" doesn't work properly)" "$log"
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
# Detect Arch-based distributions
|
|
649
|
-
is_arch_linux() {
|
|
650
|
-
if [[ -f /etc/os-release ]]; then
|
|
651
|
-
local os_id
|
|
652
|
-
os_id="$(grep -E '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true)"
|
|
653
|
-
case "$os_id" in
|
|
654
|
-
arch|manjaro|endeavouros|arcolinux|garuda|archarm|cachyos|archcraft)
|
|
655
|
-
return 0
|
|
656
|
-
esac
|
|
657
|
-
local os_id_like
|
|
658
|
-
os_id_like="$(grep -E '^ID_LIKE=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true)"
|
|
659
|
-
if [[ "$os_id_like" == *arch* ]]; then
|
|
660
|
-
return 0
|
|
661
|
-
fi
|
|
662
|
-
fi
|
|
663
|
-
if command -v pacman &> /dev/null; then
|
|
664
|
-
return 0
|
|
665
|
-
fi
|
|
666
|
-
return 1
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
install_build_tools_linux() {
|
|
670
|
-
if command -v apt-get &> /dev/null; then
|
|
671
|
-
if is_root; then
|
|
672
|
-
run_quiet_step "Updating package index" apt-get update -qq
|
|
673
|
-
run_quiet_step "Installing build tools" apt-get install -y -qq build-essential python3 make g++ cmake
|
|
674
|
-
else
|
|
675
|
-
run_quiet_step "Updating package index" sudo apt-get update -qq
|
|
676
|
-
run_quiet_step "Installing build tools" sudo apt-get install -y -qq build-essential python3 make g++ cmake
|
|
677
|
-
fi
|
|
678
|
-
return 0
|
|
679
|
-
fi
|
|
680
|
-
if command -v pacman &> /dev/null || is_arch_linux; then
|
|
681
|
-
if is_root; then
|
|
682
|
-
run_quiet_step "Installing build tools" pacman -Sy --noconfirm base-devel python make cmake gcc
|
|
683
|
-
else
|
|
684
|
-
run_quiet_step "Installing build tools" sudo pacman -Sy --noconfirm base-devel python make cmake gcc
|
|
685
|
-
fi
|
|
686
|
-
return 0
|
|
687
|
-
fi
|
|
688
|
-
if command -v dnf &> /dev/null; then
|
|
689
|
-
if is_root; then
|
|
690
|
-
run_quiet_step "Installing build tools" dnf install -y -q gcc gcc-c++ make cmake python3
|
|
691
|
-
else
|
|
692
|
-
run_quiet_step "Installing build tools" sudo dnf install -y -q gcc gcc-c++ make cmake python3
|
|
693
|
-
fi
|
|
694
|
-
return 0
|
|
695
|
-
fi
|
|
696
|
-
if command -v yum &> /dev/null; then
|
|
697
|
-
if is_root; then
|
|
698
|
-
run_quiet_step "Installing build tools" yum install -y -q gcc gcc-c++ make cmake python3
|
|
699
|
-
else
|
|
700
|
-
run_quiet_step "Installing build tools" sudo yum install -y -q gcc gcc-c++ make cmake python3
|
|
701
|
-
fi
|
|
702
|
-
return 0
|
|
703
|
-
fi
|
|
704
|
-
if command -v apk &> /dev/null; then
|
|
705
|
-
if is_root; then
|
|
706
|
-
run_quiet_step "Installing build tools" apk add --no-cache build-base python3 cmake
|
|
707
|
-
else
|
|
708
|
-
run_quiet_step "Installing build tools" sudo apk add --no-cache build-base python3 cmake
|
|
709
|
-
fi
|
|
710
|
-
return 0
|
|
711
|
-
fi
|
|
712
|
-
ui_warn "Could not detect package manager for auto-installing build tools"
|
|
713
|
-
return 1
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
install_build_tools_macos() {
|
|
717
|
-
local ok=true
|
|
718
|
-
if ! xcode-select -p >/dev/null 2>&1; then
|
|
719
|
-
ui_info "Installing Xcode Command Line Tools (required for make/clang)"
|
|
720
|
-
xcode-select --install >/dev/null 2>&1 || true
|
|
721
|
-
if ! xcode-select -p >/dev/null 2>&1; then
|
|
722
|
-
ui_warn "Xcode Command Line Tools are not ready yet"
|
|
723
|
-
ui_info "Complete the installer dialog, then re-run this installer"
|
|
724
|
-
ok=false
|
|
725
|
-
fi
|
|
726
|
-
fi
|
|
727
|
-
if ! command -v cmake >/dev/null 2>&1; then
|
|
728
|
-
if command -v brew >/dev/null 2>&1; then
|
|
729
|
-
run_quiet_step "Installing cmake" brew install cmake
|
|
730
|
-
else
|
|
731
|
-
ui_warn "Homebrew not available; cannot auto-install cmake"
|
|
732
|
-
ok=false
|
|
733
|
-
fi
|
|
734
|
-
fi
|
|
735
|
-
if ! command -v make >/dev/null 2>&1; then
|
|
736
|
-
ui_warn "make is still unavailable"
|
|
737
|
-
ok=false
|
|
738
|
-
fi
|
|
739
|
-
if ! command -v cmake >/dev/null 2>&1; then
|
|
740
|
-
ui_warn "cmake is still unavailable"
|
|
741
|
-
ok=false
|
|
742
|
-
fi
|
|
743
|
-
[[ "$ok" == "true" ]]
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
is_root() {
|
|
747
|
-
[[ $EUID -eq 0 ]]
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
run_quiet_step() {
|
|
751
|
-
local title="$1"
|
|
752
|
-
shift
|
|
753
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
754
|
-
ui_info "[dry-run] $title"
|
|
755
|
-
return 0
|
|
756
|
-
fi
|
|
757
|
-
log_detail ""
|
|
758
|
-
log_detail "[$(date '+%H:%M:%S')] \$ $* # ${title}"
|
|
759
|
-
if "$@"; then
|
|
760
|
-
return 0
|
|
761
|
-
fi
|
|
762
|
-
ui_error "${title} failed"
|
|
763
|
-
return 1
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
auto_install_build_tools_for_npm_failure() {
|
|
767
|
-
local log="$1"
|
|
768
|
-
if ! npm_log_indicates_missing_build_tools "$log"; then
|
|
769
|
-
return 1
|
|
770
|
-
fi
|
|
771
|
-
ui_warn "Detected missing native build tools; attempting automatic setup"
|
|
772
|
-
if [[ "$OS" == "linux" ]]; then
|
|
773
|
-
install_build_tools_linux || return 1
|
|
774
|
-
elif [[ "$OS" == "macos" ]]; then
|
|
775
|
-
install_build_tools_macos || return 1
|
|
776
|
-
else
|
|
777
|
-
return 1
|
|
778
|
-
fi
|
|
779
|
-
ui_success "Build tools setup complete"
|
|
780
|
-
return 0
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
extract_npm_error_code() {
|
|
784
|
-
local log="$1"
|
|
785
|
-
sed -n -E 's/^npm (ERR!|error) code[[:space:]]+([^[:space:]]+).*$/\2/p' "$log" | head -n1
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
print_npm_failure_diagnostics() {
|
|
789
|
-
local spec="$1"
|
|
790
|
-
local log="$2"
|
|
791
|
-
local error_code=""
|
|
792
|
-
ui_warn "npm install failed for ${spec}"
|
|
793
|
-
error_code="$(extract_npm_error_code "$log")"
|
|
794
|
-
if [[ -n "$error_code" ]]; then
|
|
795
|
-
echo " npm code: ${error_code}"
|
|
796
|
-
fi
|
|
797
|
-
if [[ -s "$log" ]]; then
|
|
798
|
-
echo " Last lines of log:"
|
|
799
|
-
tail -n 20 "$log" | sed 's/^/ /' || true
|
|
800
|
-
fi
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
# ─── Prerequisites ────────────────────────────────────────────────────────────
|
|
804
|
-
ensure_prerequisites() {
|
|
805
|
-
local missing=()
|
|
806
|
-
|
|
807
|
-
for cmd in curl tar; do
|
|
808
|
-
if ! command -v "$cmd" &>/dev/null; then
|
|
809
|
-
missing+=("$cmd")
|
|
810
|
-
fi
|
|
811
|
-
done
|
|
812
|
-
|
|
813
|
-
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
814
|
-
ui_info "Installing prerequisites: ${missing[*]}"
|
|
815
|
-
pkg_install "${missing[@]}"
|
|
816
|
-
fi
|
|
817
|
-
|
|
818
|
-
if ! command -v curl &>/dev/null; then
|
|
819
|
-
ui_error "Failed to install curl. Please install it manually and retry."
|
|
820
|
-
exit 1
|
|
821
|
-
fi
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
825
|
-
# Component installation functions
|
|
826
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
827
|
-
|
|
828
|
-
# ─── 1. Node.js (via nvm) ────────────────────────────────────────────────────
|
|
829
|
-
|
|
830
|
-
_load_nvm() {
|
|
831
|
-
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
832
|
-
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
|
833
|
-
# shellcheck source=/dev/null
|
|
834
|
-
\. "$NVM_DIR/nvm.sh"
|
|
835
|
-
return 0
|
|
836
|
-
fi
|
|
837
|
-
return 1
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
install_node() {
|
|
841
|
-
ui_stage "Node.js (via nvm)"
|
|
842
|
-
|
|
843
|
-
_load_nvm 2>/dev/null || true
|
|
844
|
-
|
|
845
|
-
if command -v node &>/dev/null; then
|
|
846
|
-
local current_version
|
|
847
|
-
current_version="$(node --version 2>/dev/null | sed 's/^v//')"
|
|
848
|
-
local current_major="${current_version%%.*}"
|
|
849
|
-
|
|
850
|
-
if [[ "$current_major" -ge "$NODE_VERSION" ]]; then
|
|
851
|
-
ui_success "Node.js already installed: v${current_version} (satisfies >= v${NODE_VERSION})"
|
|
852
|
-
if command -v npm &>/dev/null; then
|
|
853
|
-
ui_success "npm: $(npm --version 2>/dev/null)"
|
|
854
|
-
fi
|
|
855
|
-
_ensure_nvm_shell_config
|
|
856
|
-
return 0
|
|
857
|
-
else
|
|
858
|
-
ui_warn "Node.js version too old: v${current_version} (need >= v${NODE_VERSION})"
|
|
859
|
-
ui_info "Upgrading Node.js via nvm..."
|
|
860
|
-
fi
|
|
861
|
-
else
|
|
862
|
-
ui_info "Node.js not found — installing via nvm..."
|
|
863
|
-
fi
|
|
864
|
-
|
|
865
|
-
_do_install_node
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
_do_install_node() {
|
|
869
|
-
ui_info "Installing nvm v${NVM_VERSION} and Node.js ${NODE_VERSION}..."
|
|
870
|
-
|
|
871
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
872
|
-
ui_info "[dry-run] Would install nvm v${NVM_VERSION}"
|
|
873
|
-
ui_info "[dry-run] Would run: nvm install ${NODE_VERSION}"
|
|
874
|
-
return 0
|
|
875
|
-
fi
|
|
876
|
-
|
|
877
|
-
local nvm_install_script
|
|
878
|
-
nvm_install_script="$(mktemp)"
|
|
879
|
-
trap "rm -f '$nvm_install_script'" RETURN
|
|
880
|
-
|
|
881
|
-
local nvm_url
|
|
882
|
-
if [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
883
|
-
# Gitee mirror of nvm-sh/nvm (Chinese CDN)
|
|
884
|
-
nvm_url="https://gitee.com/mirrors/nvm/raw/v${NVM_VERSION}/install.sh"
|
|
885
|
-
ui_info "China network — downloading nvm from Gitee mirror"
|
|
886
|
-
else
|
|
887
|
-
nvm_url="https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh"
|
|
888
|
-
fi
|
|
889
|
-
if ! retry_net "Download nvm install script" 3 curl -fsSL "$nvm_url" -o "$nvm_install_script"; then
|
|
890
|
-
ui_error "Failed to download nvm install script from: $nvm_url"
|
|
891
|
-
return 1
|
|
892
|
-
fi
|
|
893
|
-
|
|
894
|
-
if [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
895
|
-
# Point nvm at the npmmirror.com Node.js CDN (much faster in China)
|
|
896
|
-
export NVM_NODEJS_ORG_MIRROR="https://npmmirror.com/mirrors/node"
|
|
897
|
-
ui_info "China network — nvm will download Node.js from npmmirror.com"
|
|
898
|
-
fi
|
|
899
|
-
|
|
900
|
-
if ! bash "$nvm_install_script"; then
|
|
901
|
-
ui_error "nvm install script failed"
|
|
902
|
-
return 1
|
|
903
|
-
fi
|
|
904
|
-
|
|
905
|
-
rm -f "$nvm_install_script"
|
|
906
|
-
trap - RETURN
|
|
907
|
-
|
|
908
|
-
if ! _load_nvm; then
|
|
909
|
-
ui_error "nvm was installed but could not be loaded (NVM_DIR=$NVM_DIR)"
|
|
910
|
-
return 1
|
|
911
|
-
fi
|
|
912
|
-
|
|
913
|
-
ui_success "nvm loaded: $(nvm --version 2>/dev/null)"
|
|
914
|
-
|
|
915
|
-
ui_info "Running: nvm install ${NODE_VERSION}"
|
|
916
|
-
if ! retry_net "nvm install ${NODE_VERSION}" 3 nvm install "${NODE_VERSION}"; then
|
|
917
|
-
ui_error "nvm install ${NODE_VERSION} failed"
|
|
918
|
-
return 1
|
|
919
|
-
fi
|
|
920
|
-
|
|
921
|
-
nvm alias default "${NODE_VERSION}" >/dev/null 2>&1 || true
|
|
922
|
-
|
|
923
|
-
local installed_version
|
|
924
|
-
installed_version="$(node --version 2>/dev/null)"
|
|
925
|
-
if [[ -z "$installed_version" ]]; then
|
|
926
|
-
ui_error "Node.js installation verification failed"
|
|
927
|
-
return 1
|
|
928
|
-
fi
|
|
929
|
-
ui_success "Node.js installed: ${installed_version}"
|
|
930
|
-
|
|
931
|
-
if command -v npm &>/dev/null; then
|
|
932
|
-
ui_success "npm: $(npm --version 2>/dev/null)"
|
|
933
|
-
else
|
|
934
|
-
ui_warn "npm was not found after installation — check manually"
|
|
935
|
-
fi
|
|
936
|
-
|
|
937
|
-
_ensure_nvm_shell_config
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
_ensure_nvm_shell_config() {
|
|
941
|
-
local init_block
|
|
942
|
-
init_block='\nexport NVM_DIR="$HOME/.nvm"\n[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"\n[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"'
|
|
943
|
-
|
|
944
|
-
local rc_files=("$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile" "$HOME/.zshrc")
|
|
945
|
-
local added=0
|
|
946
|
-
for rc in "${rc_files[@]}"; do
|
|
947
|
-
if [[ -f "$rc" ]] && ! grep -q 'NVM_DIR' "$rc" 2>/dev/null; then
|
|
948
|
-
printf '%b\n' "$init_block" >> "$rc"
|
|
949
|
-
ui_info "nvm init added to $rc"
|
|
950
|
-
added=1
|
|
951
|
-
fi
|
|
952
|
-
done
|
|
953
|
-
if [[ $added -eq 0 ]]; then
|
|
954
|
-
:
|
|
955
|
-
fi
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
# ─── 2. Docker ───────────────────────────────────────────────────────────────
|
|
959
|
-
|
|
960
|
-
install_docker() {
|
|
961
|
-
ui_stage "Docker"
|
|
962
|
-
|
|
963
|
-
local need_install_docker=0
|
|
964
|
-
local need_install_compose=0
|
|
965
|
-
|
|
966
|
-
if command -v docker &>/dev/null; then
|
|
967
|
-
local docker_version
|
|
968
|
-
docker_version="$(docker version --format '{{.Server.Version}}' 2>/dev/null | extract_semver || \
|
|
969
|
-
docker --version 2>/dev/null | extract_semver || \
|
|
970
|
-
echo "unknown")"
|
|
971
|
-
|
|
972
|
-
if [[ "$docker_version" != "unknown" ]]; then
|
|
973
|
-
ui_success "Docker already installed: v${docker_version}"
|
|
974
|
-
else
|
|
975
|
-
ui_warn "Docker command found but version unavailable (daemon may not be running)"
|
|
976
|
-
_ensure_docker_running
|
|
977
|
-
fi
|
|
978
|
-
# Always ensure mirrors are configured when NETWORK_CHINA=1,
|
|
979
|
-
# even if Docker was already installed before this run.
|
|
980
|
-
_configure_docker_mirror
|
|
981
|
-
else
|
|
982
|
-
need_install_docker=1
|
|
983
|
-
fi
|
|
984
|
-
|
|
985
|
-
if docker compose version &>/dev/null 2>&1; then
|
|
986
|
-
:
|
|
987
|
-
elif command -v docker-compose &>/dev/null; then
|
|
988
|
-
:
|
|
989
|
-
elif [[ "$OS" == "macos" ]]; then
|
|
990
|
-
:
|
|
991
|
-
else
|
|
992
|
-
need_install_compose=1
|
|
993
|
-
fi
|
|
994
|
-
|
|
995
|
-
if [[ $need_install_docker -eq 0 && $need_install_compose -eq 0 ]]; then
|
|
996
|
-
_ensure_docker_running
|
|
997
|
-
_ensure_docker_group
|
|
998
|
-
return 0
|
|
999
|
-
fi
|
|
1000
|
-
|
|
1001
|
-
if [[ $need_install_docker -eq 1 ]]; then
|
|
1002
|
-
ui_info "Docker not found — installing..."
|
|
1003
|
-
# _do_install_docker installs docker-compose-plugin in the same apt command,
|
|
1004
|
-
# so Compose V2 will be available immediately after — no separate step needed.
|
|
1005
|
-
if ! _do_install_docker; then
|
|
1006
|
-
ui_warn "Official Docker install script failed — trying system package manager fallback..."
|
|
1007
|
-
if ! _do_install_docker_apt_fallback; then
|
|
1008
|
-
ui_error "All Docker installation methods failed — skipping group setup"
|
|
1009
|
-
return 1
|
|
1010
|
-
fi
|
|
1011
|
-
fi
|
|
1012
|
-
# Compose is bundled; skip the separate install step
|
|
1013
|
-
need_install_compose=0
|
|
1014
|
-
fi
|
|
1015
|
-
|
|
1016
|
-
if [[ $need_install_compose -eq 1 ]]; then
|
|
1017
|
-
if ! docker compose version &>/dev/null 2>&1; then
|
|
1018
|
-
ui_info "Docker Compose V2 not detected — installing plugin..."
|
|
1019
|
-
if ! _do_install_compose_plugin; then
|
|
1020
|
-
ui_warn "Docker Compose V2 plugin installation failed — Compose features may be unavailable"
|
|
1021
|
-
fi
|
|
1022
|
-
fi
|
|
1023
|
-
fi
|
|
1024
|
-
|
|
1025
|
-
_ensure_docker_running
|
|
1026
|
-
_configure_docker_mirror
|
|
1027
|
-
_ensure_docker_group
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
_do_install_docker() {
|
|
1031
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1032
|
-
ui_info "[dry-run] Would install Docker via get.docker.com"
|
|
1033
|
-
return 0
|
|
1034
|
-
fi
|
|
1035
|
-
|
|
1036
|
-
if [[ "$OS" == "macos" ]]; then
|
|
1037
|
-
ui_warn "Automated Docker installation is not supported on macOS"
|
|
1038
|
-
ui_info "Please install Docker Desktop from https://www.docker.com/products/docker-desktop/"
|
|
1039
|
-
if command -v brew &>/dev/null; then
|
|
1040
|
-
ui_info "Or via Homebrew: brew install --cask docker"
|
|
1041
|
-
fi
|
|
1042
|
-
ui_info "After installation, open Docker Desktop and wait for the daemon to start, then re-run this script"
|
|
1043
|
-
return 1
|
|
1044
|
-
fi
|
|
1045
|
-
|
|
1046
|
-
# Step 1: download
|
|
1047
|
-
local docker_script
|
|
1048
|
-
docker_script="$(mktemp)"
|
|
1049
|
-
mv "$docker_script" "${docker_script}.sh"
|
|
1050
|
-
docker_script="${docker_script}.sh"
|
|
1051
|
-
ui_info "Downloading Docker install script from https://get.docker.com ..."
|
|
1052
|
-
if ! retry_net "Download Docker install script" 3 curl -fsSL https://get.docker.com -o "$docker_script"; then
|
|
1053
|
-
ui_error "Failed to download Docker install script"
|
|
1054
|
-
rm -f "$docker_script"
|
|
1055
|
-
return 1
|
|
1056
|
-
fi
|
|
1057
|
-
|
|
1058
|
-
# Step 2: preview (first 5 lines so the user can see what will run)
|
|
1059
|
-
ui_info "Install script preview (first 5 lines):"
|
|
1060
|
-
head -n 5 "$docker_script" | sed 's/^/ /' || true
|
|
1061
|
-
|
|
1062
|
-
# Step 3: run with sudo sh (as recommended by get.docker.com)
|
|
1063
|
-
local _docker_mirror_flag=""
|
|
1064
|
-
if [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
1065
|
-
_docker_mirror_flag="--mirror Aliyun"
|
|
1066
|
-
ui_info "China network detected — using Alibaba mirror (mirrors.aliyun.com)"
|
|
1067
|
-
fi
|
|
1068
|
-
ui_info "Running: sudo sh install-docker.sh ${_docker_mirror_flag}..."
|
|
1069
|
-
# shellcheck disable=SC2086
|
|
1070
|
-
if ! ${SUDO} sh "$docker_script" ${_docker_mirror_flag}; then
|
|
1071
|
-
ui_error "Docker install script failed"
|
|
1072
|
-
rm -f "$docker_script"
|
|
1073
|
-
return 1
|
|
1074
|
-
fi
|
|
1075
|
-
rm -f "$docker_script"
|
|
1076
|
-
|
|
1077
|
-
# ── Post-install steps ────────────────────────────────────────────────────
|
|
1078
|
-
# Always add REAL_USER (the non-root user who invoked sudo), not whoami which
|
|
1079
|
-
# may return "root" when running via "sudo bash install.sh".
|
|
1080
|
-
local user="${REAL_USER:-$(whoami)}"
|
|
1081
|
-
|
|
1082
|
-
# 1. sudo usermod -aG docker $REAL_USER
|
|
1083
|
-
ui_info "Running: sudo usermod -aG docker ${user}"
|
|
1084
|
-
if ${SUDO} usermod -aG docker "${user}"; then
|
|
1085
|
-
ui_success "User ${user} added to docker group"
|
|
1086
|
-
DOCKER_GROUP_JUST_ADDED=1
|
|
1087
|
-
else
|
|
1088
|
-
ui_warn "usermod failed — run manually: sudo usermod -aG docker ${user}"
|
|
1089
|
-
fi
|
|
1090
|
-
|
|
1091
|
-
# 2. sudo systemctl start docker
|
|
1092
|
-
ui_info "Running: sudo systemctl start docker"
|
|
1093
|
-
${SUDO} systemctl start docker 2>/dev/null || \
|
|
1094
|
-
${SUDO} service docker start 2>/dev/null || true
|
|
1095
|
-
|
|
1096
|
-
_ensure_docker_running
|
|
1097
|
-
|
|
1098
|
-
# 3. newgrp docker (in a script 'sg docker -c' is the non-interactive equivalent)
|
|
1099
|
-
# Activates group membership without requiring re-login
|
|
1100
|
-
ui_info "Activating docker group (newgrp docker equivalent via 'sg docker') ..."
|
|
1101
|
-
if command -v sg &>/dev/null && sg docker -c "docker info" &>/dev/null 2>&1; then
|
|
1102
|
-
DOCKER_CMD_PREFIX="sg docker -c"
|
|
1103
|
-
ui_success "docker group activated for this session"
|
|
1104
|
-
else
|
|
1105
|
-
DOCKER_USE_SUDO=1
|
|
1106
|
-
ui_info "sg docker unavailable — using sudo docker for this session"
|
|
1107
|
-
fi
|
|
1108
|
-
|
|
1109
|
-
# 4. docker ps
|
|
1110
|
-
ui_info "Running: docker ps"
|
|
1111
|
-
local docker_ps_out
|
|
1112
|
-
if [[ -n "${DOCKER_CMD_PREFIX:-}" ]]; then
|
|
1113
|
-
docker_ps_out="$(${DOCKER_CMD_PREFIX} "docker ps" 2>&1)" && {
|
|
1114
|
-
ui_success "docker ps succeeded:"
|
|
1115
|
-
echo "$docker_ps_out" | sed 's/^/ /'
|
|
1116
|
-
} || ui_warn "docker ps returned non-zero (daemon may still be warming up)"
|
|
1117
|
-
elif ${SUDO} docker ps &>/dev/null 2>&1; then
|
|
1118
|
-
docker_ps_out="$(${SUDO} docker ps 2>&1)"
|
|
1119
|
-
ui_success "docker ps succeeded (via sudo):"
|
|
1120
|
-
echo "$docker_ps_out" | sed 's/^/ /'
|
|
1121
|
-
else
|
|
1122
|
-
ui_warn "docker ps failed — run 'sudo docker ps' to check daemon status"
|
|
1123
|
-
fi
|
|
1124
|
-
|
|
1125
|
-
if ! command -v docker &>/dev/null; then
|
|
1126
|
-
ui_error "Docker installation verification failed"
|
|
1127
|
-
return 1
|
|
1128
|
-
fi
|
|
1129
|
-
|
|
1130
|
-
local installed_version
|
|
1131
|
-
installed_version="$(docker --version 2>/dev/null | extract_semver || echo "unknown")"
|
|
1132
|
-
ui_success "Docker installed: v${installed_version}"
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
# Fallback Docker installer that uses the system package manager (apt/dnf/yum).
|
|
1136
|
-
# Called when the get.docker.com script fails (network issues, GPG errors, etc.).
|
|
1137
|
-
_do_install_docker_apt_fallback() {
|
|
1138
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1139
|
-
ui_info "[dry-run] Would install Docker via system package manager (apt/dnf/yum)"
|
|
1140
|
-
return 0
|
|
1141
|
-
fi
|
|
1142
|
-
|
|
1143
|
-
if [[ "$OS" == "macos" ]]; then
|
|
1144
|
-
ui_warn "No apt/dnf fallback available on macOS — please install Docker Desktop manually"
|
|
1145
|
-
return 1
|
|
1146
|
-
fi
|
|
1147
|
-
|
|
1148
|
-
case "${PKG_MANAGER:-apt}" in
|
|
1149
|
-
apt)
|
|
1150
|
-
ui_info "Attempting: apt-get install docker.io ..."
|
|
1151
|
-
${SUDO} apt-get update -qq 2>/dev/null || true
|
|
1152
|
-
if ${SUDO} apt-get install -y docker.io docker-compose; then
|
|
1153
|
-
ui_success "Docker installed via apt (docker.io)"
|
|
1154
|
-
# Ensure service is started
|
|
1155
|
-
${SUDO} systemctl start docker 2>/dev/null || \
|
|
1156
|
-
${SUDO} service docker start 2>/dev/null || true
|
|
1157
|
-
_ensure_docker_running
|
|
1158
|
-
return 0
|
|
1159
|
-
fi
|
|
1160
|
-
ui_error "apt-get install docker.io failed"
|
|
1161
|
-
return 1
|
|
1162
|
-
;;
|
|
1163
|
-
dnf|yum)
|
|
1164
|
-
ui_info "Attempting: ${PKG_MANAGER} install docker ..."
|
|
1165
|
-
if ${SUDO} "${PKG_MANAGER}" install -y docker; then
|
|
1166
|
-
ui_success "Docker installed via ${PKG_MANAGER}"
|
|
1167
|
-
${SUDO} systemctl start docker 2>/dev/null || true
|
|
1168
|
-
_ensure_docker_running
|
|
1169
|
-
return 0
|
|
1170
|
-
fi
|
|
1171
|
-
ui_error "${PKG_MANAGER} install docker failed"
|
|
1172
|
-
return 1
|
|
1173
|
-
;;
|
|
1174
|
-
*)
|
|
1175
|
-
ui_error "No fallback Docker install method for package manager: ${PKG_MANAGER:-unknown}"
|
|
1176
|
-
return 1
|
|
1177
|
-
;;
|
|
1178
|
-
esac
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
_do_install_compose_plugin() {
|
|
1182
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1183
|
-
ui_info "[dry-run] Would install docker-compose-plugin"
|
|
1184
|
-
return 0
|
|
1185
|
-
fi
|
|
1186
|
-
|
|
1187
|
-
case "$PKG_MANAGER" in
|
|
1188
|
-
apt) pkg_install docker-compose-plugin ;;
|
|
1189
|
-
dnf|yum) pkg_install docker-compose-plugin ;;
|
|
1190
|
-
esac
|
|
1191
|
-
|
|
1192
|
-
if docker compose version &>/dev/null 2>&1; then
|
|
1193
|
-
ui_success "Docker Compose V2 installed: $(docker compose version --short 2>/dev/null)"
|
|
1194
|
-
else
|
|
1195
|
-
ui_warn "Docker Compose V2 plugin installed but not yet active — please check manually"
|
|
1196
|
-
fi
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
# Configure Docker daemon registry mirrors when NETWORK_CHINA=1.
|
|
1200
|
-
# Writes /etc/docker/daemon.json; completely replaces registry-mirrors key.
|
|
1201
|
-
_configure_docker_mirror() {
|
|
1202
|
-
if [[ "${NETWORK_CHINA:-0}" != "1" ]]; then
|
|
1203
|
-
return 0
|
|
1204
|
-
fi
|
|
1205
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1206
|
-
ui_info "[dry-run] Would configure Docker daemon mirrors for China"
|
|
1207
|
-
return 0
|
|
1208
|
-
fi
|
|
1209
|
-
|
|
1210
|
-
local daemon_json="/etc/docker/daemon.json"
|
|
1211
|
-
local mirrors_json='["https://docker.m.daocloud.io","https://hub-mirror.c.163.com","https://mirror.baidubce.com"]'
|
|
1212
|
-
|
|
1213
|
-
ui_info "Configuring Docker daemon registry mirrors (China)..."
|
|
1214
|
-
${SUDO} mkdir -p /etc/docker
|
|
1215
|
-
|
|
1216
|
-
# Merge into existing config preserving other keys, then overwrite registry-mirrors.
|
|
1217
|
-
# Note: node -e uses double-quoted string so shell variables expand correctly.
|
|
1218
|
-
local tmp_cfg
|
|
1219
|
-
tmp_cfg="$(mktemp)"
|
|
1220
|
-
if [[ -f "$daemon_json" ]]; then
|
|
1221
|
-
node -e "
|
|
1222
|
-
const fs = require('fs');
|
|
1223
|
-
let cfg = {};
|
|
1224
|
-
try { cfg = JSON.parse(fs.readFileSync('${daemon_json}', 'utf8')); } catch (e) {}
|
|
1225
|
-
cfg['registry-mirrors'] = ${mirrors_json};
|
|
1226
|
-
fs.writeFileSync('${tmp_cfg}', JSON.stringify(cfg, null, 2) + '\\n');
|
|
1227
|
-
" 2>/dev/null || printf '{"registry-mirrors":%s}\n' "${mirrors_json}" > "$tmp_cfg"
|
|
1228
|
-
else
|
|
1229
|
-
printf '{"registry-mirrors":%s}\n' "${mirrors_json}" > "$tmp_cfg"
|
|
1230
|
-
fi
|
|
1231
|
-
${SUDO} cp "$tmp_cfg" "$daemon_json"
|
|
1232
|
-
rm -f "$tmp_cfg"
|
|
1233
|
-
|
|
1234
|
-
# Reload daemon to apply new config
|
|
1235
|
-
if command -v systemctl &>/dev/null; then
|
|
1236
|
-
${SUDO} systemctl daemon-reload 2>/dev/null || true
|
|
1237
|
-
${SUDO} systemctl restart docker 2>/dev/null || true
|
|
1238
|
-
sleep 2
|
|
1239
|
-
fi
|
|
1240
|
-
ui_success "Docker daemon mirrors configured: daocloud.io, 163.com, baidubce.com"
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
_ensure_docker_running() {
|
|
1244
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1245
|
-
return 0
|
|
1246
|
-
fi
|
|
1247
|
-
|
|
1248
|
-
if [[ "$OS" == "macos" ]]; then
|
|
1249
|
-
local waited=0
|
|
1250
|
-
while ! docker info &>/dev/null 2>&1; do
|
|
1251
|
-
if [[ $waited -ge 15 ]]; then
|
|
1252
|
-
ui_warn "Docker daemon did not become ready within 15 seconds"
|
|
1253
|
-
ui_info "Make sure Docker Desktop is open and running"
|
|
1254
|
-
return 1
|
|
1255
|
-
fi
|
|
1256
|
-
sleep 1
|
|
1257
|
-
(( waited++ )) || true
|
|
1258
|
-
done
|
|
1259
|
-
[[ $waited -lt 15 ]] && ui_success "Docker daemon is ready"
|
|
1260
|
-
return 0
|
|
1261
|
-
fi
|
|
1262
|
-
|
|
1263
|
-
if command -v systemctl &>/dev/null; then
|
|
1264
|
-
# Reset any failed state left by previous install attempts
|
|
1265
|
-
${SUDO} systemctl reset-failed docker.socket docker.service 2>/dev/null || true
|
|
1266
|
-
|
|
1267
|
-
# Start socket-activated unit first (prevents "dependency failed" errors)
|
|
1268
|
-
if ! systemctl is-active --quiet docker.socket 2>/dev/null; then
|
|
1269
|
-
ui_info "Starting Docker socket..."
|
|
1270
|
-
run_sudo systemctl start docker.socket 2>/dev/null || true
|
|
1271
|
-
sleep 1
|
|
1272
|
-
fi
|
|
1273
|
-
|
|
1274
|
-
if ! systemctl is-active --quiet docker 2>/dev/null; then
|
|
1275
|
-
ui_info "Starting Docker service..."
|
|
1276
|
-
if ! ${SUDO} systemctl start docker; then
|
|
1277
|
-
ui_error "Failed to start Docker service — check: sudo journalctl -xe"
|
|
1278
|
-
else
|
|
1279
|
-
ui_success "Docker service started"
|
|
1280
|
-
fi
|
|
1281
|
-
fi
|
|
1282
|
-
if ! systemctl is-enabled --quiet docker 2>/dev/null; then
|
|
1283
|
-
ui_info "Enabling Docker on startup..."
|
|
1284
|
-
${SUDO} systemctl enable docker 2>/dev/null || true
|
|
1285
|
-
fi
|
|
1286
|
-
if ! systemctl is-enabled --quiet docker.socket 2>/dev/null; then
|
|
1287
|
-
${SUDO} systemctl enable docker.socket 2>/dev/null || true
|
|
1288
|
-
fi
|
|
1289
|
-
|
|
1290
|
-
# Wait up to 15s for Docker daemon to be ready to accept connections
|
|
1291
|
-
local waited=0
|
|
1292
|
-
while ! ${SUDO} docker info &>/dev/null 2>&1; do
|
|
1293
|
-
if [[ $waited -ge 15 ]]; then
|
|
1294
|
-
ui_warn "Docker daemon did not become ready within 15 seconds"
|
|
1295
|
-
break
|
|
1296
|
-
fi
|
|
1297
|
-
sleep 1
|
|
1298
|
-
(( waited++ )) || true
|
|
1299
|
-
done
|
|
1300
|
-
[[ $waited -lt 15 ]] && ui_success "Docker daemon is ready"
|
|
1301
|
-
else
|
|
1302
|
-
if ! ${SUDO} service docker status &>/dev/null; then
|
|
1303
|
-
ui_info "Starting Docker service..."
|
|
1304
|
-
run_sudo service docker start || true
|
|
1305
|
-
fi
|
|
1306
|
-
fi
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
_ensure_docker_group() {
|
|
1310
|
-
if [[ "$DRY_RUN" == "1" || $EUID -eq 0 || "$OS" == "macos" ]]; then
|
|
1311
|
-
return 0
|
|
1312
|
-
fi
|
|
1313
|
-
|
|
1314
|
-
# Always operate on the real (non-root) user — not whoami, which may return
|
|
1315
|
-
# "root" when the script was launched with "sudo bash install.sh".
|
|
1316
|
-
local user="${REAL_USER:-$(id -un)}"
|
|
1317
|
-
|
|
1318
|
-
# Ensure docker group exists (may be missing when Docker was installed differently)
|
|
1319
|
-
if ! getent group docker &>/dev/null 2>&1; then
|
|
1320
|
-
ui_info "Creating docker group..."
|
|
1321
|
-
run_sudo groupadd docker 2>/dev/null || true
|
|
1322
|
-
fi
|
|
1323
|
-
|
|
1324
|
-
# Use getent to check group membership rather than the shell-builtin `groups`
|
|
1325
|
-
# command, which reflects the login-session groups and may not include groups
|
|
1326
|
-
# added earlier in the same install run (especially under sudo).
|
|
1327
|
-
if ! getent group docker 2>/dev/null | grep -qw "${user}"; then
|
|
1328
|
-
ui_info "Adding ${user} to the docker group..."
|
|
1329
|
-
if ${SUDO} usermod -aG docker "${user}"; then
|
|
1330
|
-
ui_success "User ${user} added to docker group"
|
|
1331
|
-
else
|
|
1332
|
-
ui_warn "Failed to add ${user} to docker group — run manually: sudo usermod -aG docker ${user}"
|
|
1333
|
-
fi
|
|
1334
|
-
DOCKER_GROUP_JUST_ADDED=1
|
|
1335
|
-
else
|
|
1336
|
-
ui_info "User ${user} is already in the docker group"
|
|
1337
|
-
fi
|
|
1338
|
-
|
|
1339
|
-
# Check if docker is already accessible directly
|
|
1340
|
-
if docker info &>/dev/null 2>&1; then
|
|
1341
|
-
return 0
|
|
1342
|
-
fi
|
|
1343
|
-
|
|
1344
|
-
# Try activating the docker group for the current session via "sg docker"
|
|
1345
|
-
# This avoids requiring the user to log out and back in within the same install run
|
|
1346
|
-
if command -v sg &>/dev/null 2>/dev/null && sg docker -c "docker info" &>/dev/null 2>&1; then
|
|
1347
|
-
DOCKER_CMD_PREFIX="sg docker -c"
|
|
1348
|
-
ui_success "Docker group activated for this session (via 'sg docker')"
|
|
1349
|
-
ui_info "Future terminals will have Docker access automatically"
|
|
1350
|
-
return 0
|
|
1351
|
-
fi
|
|
1352
|
-
|
|
1353
|
-
# sg docker failed or unavailable — try ACL grant on the docker socket
|
|
1354
|
-
# (requires the 'acl' package; harmless if absent)
|
|
1355
|
-
if command -v setfacl &>/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
|
1356
|
-
ui_info "Trying ACL grant on /var/run/docker.sock for ${user} ..."
|
|
1357
|
-
if ${SUDO} setfacl -m "user:${user}:rw" /var/run/docker.sock 2>/dev/null; then
|
|
1358
|
-
if docker info &>/dev/null 2>&1; then
|
|
1359
|
-
ui_success "Docker access granted via socket ACL (current session)"
|
|
1360
|
-
ui_info "Future terminals will have Docker access automatically (group membership active on next login)"
|
|
1361
|
-
return 0
|
|
1362
|
-
fi
|
|
1363
|
-
fi
|
|
1364
|
-
fi
|
|
1365
|
-
|
|
1366
|
-
# Last resort — fall back to sudo docker for this session
|
|
1367
|
-
# This ensures OpenClaw image pull can proceed without requiring re-login
|
|
1368
|
-
DOCKER_USE_SUDO=1
|
|
1369
|
-
if [[ "${DOCKER_GROUP_JUST_ADDED:-0}" == "1" ]]; then
|
|
1370
|
-
ui_warn "Docker group assigned but not yet active in this shell session."
|
|
1371
|
-
ui_warn "Please log out and log back in (or run: newgrp docker) for group membership to take effect."
|
|
1372
|
-
fi
|
|
1373
|
-
ui_info "Using 'sudo docker' for this session; future terminals will have direct access."
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
# Run a docker command, with automatic fallback priority:
|
|
1377
|
-
# 1. "sg docker -c" — group activated in-session (DOCKER_CMD_PREFIX set)
|
|
1378
|
-
# 2. sudo docker — sudo fallback when group not yet active (DOCKER_USE_SUDO=1)
|
|
1379
|
-
# 3. docker — direct access (group already active or running as root)
|
|
1380
|
-
#
|
|
1381
|
-
# Usage: docker_exec info
|
|
1382
|
-
# docker_exec pull image:tag
|
|
1383
|
-
# docker_exec image inspect tag
|
|
1384
|
-
docker_exec() {
|
|
1385
|
-
if [[ -n "${DOCKER_CMD_PREFIX:-}" ]]; then
|
|
1386
|
-
# sg docker -c expects a single string argument
|
|
1387
|
-
${DOCKER_CMD_PREFIX} "docker $*"
|
|
1388
|
-
elif [[ "${DOCKER_USE_SUDO:-0}" == "1" ]]; then
|
|
1389
|
-
${SUDO} docker "$@"
|
|
1390
|
-
else
|
|
1391
|
-
docker "$@"
|
|
1392
|
-
fi
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
# ─── 3. Nomad ────────────────────────────────────────────────────────────────
|
|
1396
|
-
|
|
1397
|
-
install_nomad() {
|
|
1398
|
-
ui_stage "Nomad"
|
|
1399
|
-
|
|
1400
|
-
local local_bin="${JISHUSHELL_BIN_DIR}/nomad"
|
|
1401
|
-
|
|
1402
|
-
# In China, skip the HashiCorp apt/yum repo (GPG key download is unreliable)
|
|
1403
|
-
# and go straight to the binary download fallback.
|
|
1404
|
-
if [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
1405
|
-
ui_info "China network — skipping HashiCorp repo, using binary download directly"
|
|
1406
|
-
_install_nomad_binary || return 1
|
|
1407
|
-
_ensure_jishushell_bin_in_path
|
|
1408
|
-
start_nomad
|
|
1409
|
-
install_nomad_systemd
|
|
1410
|
-
return 0
|
|
1411
|
-
fi
|
|
1412
|
-
|
|
1413
|
-
# ── 1. Check local user-space install ────────────────────────────────────
|
|
1414
|
-
if [[ -e "$local_bin" ]]; then
|
|
1415
|
-
if [[ ! -f "$local_bin" ]]; then
|
|
1416
|
-
ui_warn "Path ${local_bin} exists but is not a regular file — removing..."
|
|
1417
|
-
rm -rf "$local_bin" 2>/dev/null || true
|
|
1418
|
-
elif [[ ! -x "$local_bin" ]]; then
|
|
1419
|
-
ui_warn "Nomad found at ${local_bin} but is not executable — fixing permissions..."
|
|
1420
|
-
if ! chmod 755 "$local_bin" 2>/dev/null; then
|
|
1421
|
-
ui_warn "Could not fix permissions — removing and reinstalling"
|
|
1422
|
-
rm -f "$local_bin"
|
|
1423
|
-
fi
|
|
1424
|
-
fi
|
|
1425
|
-
|
|
1426
|
-
if [[ -x "$local_bin" ]]; then
|
|
1427
|
-
local current_version
|
|
1428
|
-
current_version="$("$local_bin" version 2>/dev/null | head -n1 | extract_semver || echo "")"
|
|
1429
|
-
|
|
1430
|
-
if [[ -z "$current_version" ]]; then
|
|
1431
|
-
ui_warn "Nomad at ${local_bin} is not functional (wrong arch or corrupt) — reinstalling..."
|
|
1432
|
-
rm -f "$local_bin"
|
|
1433
|
-
elif version_gte "$current_version" "$NOMAD_VERSION"; then
|
|
1434
|
-
ui_success "Nomad already installed: v${current_version} → ${local_bin}"
|
|
1435
|
-
_ensure_jishushell_bin_in_path
|
|
1436
|
-
return 0
|
|
1437
|
-
else
|
|
1438
|
-
ui_warn "Nomad version too old: v${current_version} (need >= v${NOMAD_VERSION}) — upgrading..."
|
|
1439
|
-
rm -f "$local_bin"
|
|
1440
|
-
fi
|
|
1441
|
-
fi
|
|
1442
|
-
fi
|
|
1443
|
-
|
|
1444
|
-
# ── 2. Check system nomad (informational only) ────────────────────────────
|
|
1445
|
-
if [[ ! -e "$local_bin" ]] && command -v nomad &>/dev/null; then
|
|
1446
|
-
local sys_ver
|
|
1447
|
-
sys_ver="$(nomad version 2>/dev/null | head -n1 | extract_semver || echo "?")"
|
|
1448
|
-
ui_info "System Nomad found (v${sys_ver}) — installing local copy to ${local_bin} for JishuShell..."
|
|
1449
|
-
else
|
|
1450
|
-
ui_info "Installing Nomad v${NOMAD_VERSION} to ${local_bin}..."
|
|
1451
|
-
fi
|
|
1452
|
-
|
|
1453
|
-
_do_install_nomad
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
_do_install_nomad() {
|
|
1457
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1458
|
-
ui_info "[dry-run] Would install Nomad v${NOMAD_VERSION} to ${JISHUSHELL_BIN_DIR}/nomad"
|
|
1459
|
-
ui_info "[dry-run] Would add ${JISHUSHELL_BIN_DIR} to PATH in shell configs"
|
|
1460
|
-
return 0
|
|
1461
|
-
fi
|
|
1462
|
-
|
|
1463
|
-
# Install directly to ~/.jishushell/bin — no sudo required
|
|
1464
|
-
_install_nomad_binary
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
_install_nomad_via_repo() {
|
|
1468
|
-
case "$PKG_MANAGER" in
|
|
1469
|
-
apt)
|
|
1470
|
-
if ! pkg_install gpg; then
|
|
1471
|
-
return 1
|
|
1472
|
-
fi
|
|
1473
|
-
|
|
1474
|
-
local keyring="/usr/share/keyrings/hashicorp-archive-keyring.gpg"
|
|
1475
|
-
if [[ ! -f "$keyring" ]]; then
|
|
1476
|
-
ui_info "Adding HashiCorp GPG key..."
|
|
1477
|
-
if ! retry_net "Download HashiCorp GPG key" 3 bash -c "curl -fsSL https://apt.releases.hashicorp.com/gpg | ${SUDO} gpg --dearmor -o '$keyring' 2>/dev/null"; then
|
|
1478
|
-
return 1
|
|
1479
|
-
fi
|
|
1480
|
-
fi
|
|
1481
|
-
|
|
1482
|
-
local sources_file="/etc/apt/sources.list.d/hashicorp.list"
|
|
1483
|
-
if [[ ! -f "$sources_file" ]]; then
|
|
1484
|
-
ui_info "Adding HashiCorp APT repository..."
|
|
1485
|
-
echo "deb [signed-by=${keyring}] https://apt.releases.hashicorp.com $(lsb_release -cs 2>/dev/null || echo stable) main" | \
|
|
1486
|
-
${SUDO} tee "$sources_file" >/dev/null
|
|
1487
|
-
fi
|
|
1488
|
-
|
|
1489
|
-
wait_for_apt_lock
|
|
1490
|
-
run_sudo apt-get update
|
|
1491
|
-
run_sudo apt-get install -y nomad
|
|
1492
|
-
;;
|
|
1493
|
-
dnf|yum)
|
|
1494
|
-
local repo_file="/etc/yum.repos.d/hashicorp.repo"
|
|
1495
|
-
if [[ ! -f "$repo_file" ]]; then
|
|
1496
|
-
ui_info "Adding HashiCorp YUM repository..."
|
|
1497
|
-
${SUDO} tee "$repo_file" >/dev/null <<'REPO'
|
|
1498
|
-
[hashicorp]
|
|
1499
|
-
name=HashiCorp Stable - $basearch
|
|
1500
|
-
baseurl=https://rpm.releases.hashicorp.com/RHEL/$releasever/$basearch/stable
|
|
1501
|
-
enabled=1
|
|
1502
|
-
gpgcheck=1
|
|
1503
|
-
gpgkey=https://rpm.releases.hashicorp.com/gpg
|
|
1504
|
-
REPO
|
|
1505
|
-
fi
|
|
1506
|
-
|
|
1507
|
-
run_sudo "$PKG_MANAGER" install -y nomad
|
|
1508
|
-
;;
|
|
1509
|
-
*)
|
|
1510
|
-
return 1
|
|
1511
|
-
;;
|
|
1512
|
-
esac
|
|
1513
|
-
|
|
1514
|
-
if command -v nomad &>/dev/null; then
|
|
1515
|
-
local ver
|
|
1516
|
-
ver="$(nomad version 2>/dev/null | head -n1 | extract_semver || echo "unknown")"
|
|
1517
|
-
ui_success "Nomad installed (via system package): v${ver}"
|
|
1518
|
-
return 0
|
|
1519
|
-
fi
|
|
1520
|
-
|
|
1521
|
-
return 1
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
_install_nomad_binary() {
|
|
1525
|
-
local dest="${JISHUSHELL_BIN_DIR}/nomad"
|
|
1526
|
-
|
|
1527
|
-
# Ensure destination directory exists (no sudo needed — it's in $HOME)
|
|
1528
|
-
if ! mkdir -p "${JISHUSHELL_BIN_DIR}"; then
|
|
1529
|
-
ui_error "Failed to create directory: ${JISHUSHELL_BIN_DIR}"
|
|
1530
|
-
return 1
|
|
1531
|
-
fi
|
|
1532
|
-
|
|
1533
|
-
# Remove stale/non-executable leftover
|
|
1534
|
-
if [[ -e "$dest" && ! -x "$dest" ]]; then
|
|
1535
|
-
ui_warn "Removing non-executable Nomad binary at ${dest}"
|
|
1536
|
-
rm -f "$dest" 2>/dev/null || true
|
|
1537
|
-
fi
|
|
1538
|
-
|
|
1539
|
-
local platform
|
|
1540
|
-
platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
1541
|
-
local download_url="https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_${platform}_${ARCH}.zip"
|
|
1542
|
-
local tmp_dir
|
|
1543
|
-
tmp_dir="$(mktemp -d)"
|
|
1544
|
-
|
|
1545
|
-
ui_info "Downloading Nomad v${NOMAD_VERSION} (${platform}/${ARCH})..."
|
|
1546
|
-
|
|
1547
|
-
if ! retry_net "Download Nomad binary" 3 curl -fsSL "$download_url" -o "${tmp_dir}/nomad.zip"; then
|
|
1548
|
-
ui_error "Failed to download Nomad: $download_url"
|
|
1549
|
-
rm -rf "$tmp_dir"
|
|
1550
|
-
return 1
|
|
1551
|
-
fi
|
|
1552
|
-
|
|
1553
|
-
local checksums_url="https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_SHA256SUMS"
|
|
1554
|
-
if retry_net "Download Nomad checksums" 3 curl -fsSL "$checksums_url" -o "${tmp_dir}/SHA256SUMS" 2>/dev/null; then
|
|
1555
|
-
local expected_hash
|
|
1556
|
-
expected_hash="$(grep "nomad_${NOMAD_VERSION}_${platform}_${ARCH}.zip" "${tmp_dir}/SHA256SUMS" | awk '{print $1}')"
|
|
1557
|
-
if [[ -n "$expected_hash" ]]; then
|
|
1558
|
-
local actual_hash
|
|
1559
|
-
if command -v sha256sum &>/dev/null; then
|
|
1560
|
-
actual_hash="$(sha256sum "${tmp_dir}/nomad.zip" | awk '{print $1}')"
|
|
1561
|
-
else
|
|
1562
|
-
actual_hash="$(shasum -a 256 "${tmp_dir}/nomad.zip" | awk '{print $1}')"
|
|
1563
|
-
fi
|
|
1564
|
-
if [[ "$expected_hash" != "$actual_hash" ]]; then
|
|
1565
|
-
ui_error "Nomad checksum mismatch — download may have been tampered with!"
|
|
1566
|
-
ui_error " Expected: $expected_hash"
|
|
1567
|
-
ui_error " Got: $actual_hash"
|
|
1568
|
-
rm -rf "$tmp_dir"
|
|
1569
|
-
return 1
|
|
1570
|
-
fi
|
|
1571
|
-
ui_info "Checksum verified ✓"
|
|
1572
|
-
fi
|
|
1573
|
-
else
|
|
1574
|
-
ui_error "Could not download Nomad checksum file — aborting installation for security"
|
|
1575
|
-
ui_error "If this is a network issue, retry. To skip verification (not recommended), use --skip-verify."
|
|
1576
|
-
rm -rf "$tmp_dir"
|
|
1577
|
-
return 1
|
|
1578
|
-
fi
|
|
1579
|
-
|
|
1580
|
-
if ! command -v unzip &>/dev/null; then
|
|
1581
|
-
ui_info "Installing unzip..."
|
|
1582
|
-
pkg_install unzip
|
|
1583
|
-
fi
|
|
1584
|
-
|
|
1585
|
-
if ! unzip -o "${tmp_dir}/nomad.zip" nomad -d "${tmp_dir}" >/dev/null 2>&1; then
|
|
1586
|
-
# Fallback: extract all (some zips don't support file-specific extraction)
|
|
1587
|
-
if ! unzip -o "${tmp_dir}/nomad.zip" -d "${tmp_dir}" >/dev/null 2>&1; then
|
|
1588
|
-
ui_error "Failed to extract Nomad archive"
|
|
1589
|
-
rm -rf "$tmp_dir"
|
|
1590
|
-
return 1
|
|
1591
|
-
fi
|
|
1592
|
-
fi
|
|
1593
|
-
|
|
1594
|
-
if [[ ! -f "${tmp_dir}/nomad" ]]; then
|
|
1595
|
-
ui_error "Nomad binary not found in downloaded archive"
|
|
1596
|
-
rm -rf "$tmp_dir"
|
|
1597
|
-
return 1
|
|
1598
|
-
fi
|
|
1599
|
-
|
|
1600
|
-
# Atomic install: write to temp name then rename to avoid partial reads during install
|
|
1601
|
-
local dest_tmp="${dest}.tmp.$$"
|
|
1602
|
-
if ! cp "${tmp_dir}/nomad" "$dest_tmp"; then
|
|
1603
|
-
ui_error "Failed to copy Nomad binary to ${JISHUSHELL_BIN_DIR}"
|
|
1604
|
-
rm -rf "$tmp_dir" "$dest_tmp" 2>/dev/null || true
|
|
1605
|
-
return 1
|
|
1606
|
-
fi
|
|
1607
|
-
chmod 755 "$dest_tmp"
|
|
1608
|
-
if ! mv -f "$dest_tmp" "$dest"; then
|
|
1609
|
-
ui_error "Failed to move Nomad binary to ${dest}"
|
|
1610
|
-
rm -f "$dest_tmp" 2>/dev/null || true
|
|
1611
|
-
rm -rf "$tmp_dir"
|
|
1612
|
-
return 1
|
|
1613
|
-
fi
|
|
1614
|
-
rm -rf "$tmp_dir"
|
|
1615
|
-
|
|
1616
|
-
# Verify the installed binary is executable and actually runs
|
|
1617
|
-
if [[ ! -x "$dest" ]]; then
|
|
1618
|
-
ui_error "Nomad binary at ${dest} is not executable after install"
|
|
1619
|
-
return 1
|
|
1620
|
-
fi
|
|
1621
|
-
|
|
1622
|
-
local installed_version
|
|
1623
|
-
installed_version="$("$dest" version 2>/dev/null | head -n1 | extract_semver || echo "")"
|
|
1624
|
-
if [[ -z "$installed_version" ]]; then
|
|
1625
|
-
ui_error "Nomad binary at ${dest} does not run — possibly wrong architecture (${ARCH})"
|
|
1626
|
-
rm -f "$dest" 2>/dev/null || true
|
|
1627
|
-
return 1
|
|
1628
|
-
fi
|
|
1629
|
-
|
|
1630
|
-
_ensure_jishushell_bin_in_path
|
|
1631
|
-
ui_success "Nomad installed: v${installed_version} → ${dest}"
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
# Add ~/.jishushell/bin to PATH in shell startup files and current session
|
|
1635
|
-
_ensure_jishushell_bin_in_path() {
|
|
1636
|
-
local bin_dir="${JISHUSHELL_BIN_DIR}"
|
|
1637
|
-
local marker="# jishushell-bin-path"
|
|
1638
|
-
local init_line="export PATH=\"${bin_dir}:\$PATH\""
|
|
1639
|
-
|
|
1640
|
-
# Export for the current running shell immediately
|
|
1641
|
-
export PATH="${bin_dir}:${PATH}"
|
|
1642
|
-
|
|
1643
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1644
|
-
ui_info "[dry-run] Would add ${bin_dir} to PATH in shell startup files"
|
|
1645
|
-
return 0
|
|
1646
|
-
fi
|
|
1647
|
-
|
|
1648
|
-
local rc_files=("$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile" "$HOME/.zshrc")
|
|
1649
|
-
local added=0
|
|
1650
|
-
for rc in "${rc_files[@]}"; do
|
|
1651
|
-
if [[ -f "$rc" ]] && ! grep -qF "$marker" "$rc" 2>/dev/null; then
|
|
1652
|
-
printf '\n%s\n%s\n' "$marker" "$init_line" >> "$rc"
|
|
1653
|
-
ui_info "Added ${bin_dir} to PATH in ${rc}"
|
|
1654
|
-
added=1
|
|
1655
|
-
fi
|
|
1656
|
-
done
|
|
1657
|
-
|
|
1658
|
-
if [[ $added -eq 0 ]]; then
|
|
1659
|
-
ui_info "JishuShell bin PATH already configured"
|
|
1660
|
-
fi
|
|
1661
|
-
ui_info "Tip: run 'source ~/.bashrc' or open a new terminal to activate PATH"
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
# Check if a TCP port is in LISTEN state (cross-platform: Linux ss, macOS lsof)
|
|
1665
|
-
_port_is_listening() {
|
|
1666
|
-
local port="$1"
|
|
1667
|
-
if command -v ss &>/dev/null; then
|
|
1668
|
-
ss -tlnp 2>/dev/null | grep -q ":${port} "
|
|
1669
|
-
elif command -v lsof &>/dev/null; then
|
|
1670
|
-
lsof -iTCP:"${port}" -sTCP:LISTEN &>/dev/null 2>&1
|
|
1671
|
-
else
|
|
1672
|
-
nc -z 127.0.0.1 "${port}" &>/dev/null 2>&1
|
|
1673
|
-
fi
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
# Write nomad.hcl if it does not already exist.
|
|
1677
|
-
# Safe to call multiple times — never overwrites an existing config.
|
|
1678
|
-
_ensure_nomad_hcl() {
|
|
1679
|
-
local nomad_config_dir="${JISHUSHELL_HOME}/nomad"
|
|
1680
|
-
local nomad_data_dir="${JISHUSHELL_HOME}/nomad/data"
|
|
1681
|
-
local nomad_alloc_dir="${JISHUSHELL_HOME}/nomad/data/alloc"
|
|
1682
|
-
local config_file="${nomad_config_dir}/nomad.hcl"
|
|
1683
|
-
|
|
1684
|
-
[[ -f "$config_file" ]] && return 0
|
|
1685
|
-
|
|
1686
|
-
mkdir -p "$nomad_config_dir" "$nomad_data_dir" "$nomad_alloc_dir"
|
|
1687
|
-
# Dirs are created by the current user — no sudo needed
|
|
1688
|
-
chown -R "${REAL_USER}:${REAL_GID:-${REAL_USER}}" "${JISHUSHELL_HOME}" 2>/dev/null || true
|
|
1689
|
-
|
|
1690
|
-
cat > "$config_file" << NOMAD_HCL
|
|
1691
|
-
data_dir = "${nomad_data_dir}"
|
|
1692
|
-
|
|
1693
|
-
bind_addr = "127.0.0.1"
|
|
1694
|
-
|
|
1695
|
-
leave_on_terminate = false
|
|
1696
|
-
|
|
1697
|
-
advertise {
|
|
1698
|
-
http = "127.0.0.1"
|
|
1699
|
-
rpc = "127.0.0.1"
|
|
1700
|
-
serf = "127.0.0.1"
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
server {
|
|
1704
|
-
enabled = true
|
|
1705
|
-
bootstrap_expect = 1
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
client {
|
|
1709
|
-
enabled = true
|
|
1710
|
-
servers = ["127.0.0.1:4647"]
|
|
1711
|
-
alloc_dir = "${nomad_alloc_dir}"
|
|
1712
|
-
|
|
1713
|
-
drain_on_shutdown {
|
|
1714
|
-
deadline = "30s"
|
|
1715
|
-
force = true
|
|
1716
|
-
ignore_system_jobs = true
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
plugin "docker" {
|
|
1721
|
-
config {
|
|
1722
|
-
disable_log_collection = true
|
|
1723
|
-
volumes {
|
|
1724
|
-
enabled = true
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
acl {
|
|
1730
|
-
enabled = true
|
|
1731
|
-
}
|
|
1732
|
-
NOMAD_HCL
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
# Start Nomad agent (matches setup-manager.ts startNomad())
|
|
1736
|
-
start_nomad() {
|
|
1737
|
-
local nomad_bin="${JISHUSHELL_BIN_DIR}/nomad"
|
|
1738
|
-
local nomad_config_dir="${JISHUSHELL_HOME}/nomad"
|
|
1739
|
-
local config_file="${nomad_config_dir}/nomad.hcl"
|
|
1740
|
-
|
|
1741
|
-
# Check if already running (port 4646)
|
|
1742
|
-
if _port_is_listening 4646; then
|
|
1743
|
-
ui_success "Nomad is already running on port 4646"
|
|
1744
|
-
return 0
|
|
1745
|
-
fi
|
|
1746
|
-
|
|
1747
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1748
|
-
ui_info "[dry-run] Would write ${config_file} and start nomad agent"
|
|
1749
|
-
return 0
|
|
1750
|
-
fi
|
|
1751
|
-
|
|
1752
|
-
# On Linux with systemd: the single path for starting Nomad.
|
|
1753
|
-
# First kill any stale non-systemd Nomad processes that may hold the port.
|
|
1754
|
-
if command -v systemctl &>/dev/null && systemctl is-enabled nomad &>/dev/null 2>&1; then
|
|
1755
|
-
local stale_pids
|
|
1756
|
-
stale_pids="$(pgrep -f 'nomad agent' 2>/dev/null || true)"
|
|
1757
|
-
if [[ -n "$stale_pids" ]]; then
|
|
1758
|
-
local systemd_pid
|
|
1759
|
-
systemd_pid="$(systemctl show nomad --property=MainPID --value 2>/dev/null || echo "")"
|
|
1760
|
-
local pid
|
|
1761
|
-
for pid in $stale_pids; do
|
|
1762
|
-
if [[ "$pid" != "$systemd_pid" ]]; then
|
|
1763
|
-
ui_info "Killing stale Nomad process (PID ${pid}) before systemd start..."
|
|
1764
|
-
kill "$pid" 2>/dev/null || true
|
|
1765
|
-
fi
|
|
1766
|
-
done
|
|
1767
|
-
sleep 1
|
|
1768
|
-
fi
|
|
1769
|
-
|
|
1770
|
-
ui_info "Starting Nomad via systemd..."
|
|
1771
|
-
${SUDO} systemctl start nomad 2>/dev/null || true
|
|
1772
|
-
local i
|
|
1773
|
-
for i in $(seq 1 30); do
|
|
1774
|
-
sleep 1
|
|
1775
|
-
if _port_is_listening 4646; then
|
|
1776
|
-
ui_success "Nomad started (systemd)"
|
|
1777
|
-
return 0
|
|
1778
|
-
fi
|
|
1779
|
-
done
|
|
1780
|
-
ui_warn "Nomad did not start within 30s — port 4646 not listening"
|
|
1781
|
-
ui_info "Check: sudo journalctl -u nomad -n 30"
|
|
1782
|
-
return 1
|
|
1783
|
-
fi
|
|
1784
|
-
|
|
1785
|
-
# On macOS with launchd: plist is already loaded with RunAtLoad=true, so launchd
|
|
1786
|
-
# started Nomad. Just wait for the port — do NOT spawn a competing nohup process.
|
|
1787
|
-
local plist_path="${HOME}/Library/LaunchAgents/com.jishushell.nomad.plist"
|
|
1788
|
-
if [[ "$OS" == "macos" ]] && [[ -f "$plist_path" ]]; then
|
|
1789
|
-
ui_info "Waiting for Nomad (launchd)..."
|
|
1790
|
-
local i
|
|
1791
|
-
for i in $(seq 1 30); do
|
|
1792
|
-
sleep 1
|
|
1793
|
-
if _port_is_listening 4646; then
|
|
1794
|
-
ui_success "Nomad started"
|
|
1795
|
-
return 0
|
|
1796
|
-
fi
|
|
1797
|
-
done
|
|
1798
|
-
ui_warn "Nomad did not start within 30s — port 4646 not listening"
|
|
1799
|
-
ui_info "Check log: ${JISHUSHELL_HOME}/nomad/nomad.log"
|
|
1800
|
-
return 1
|
|
1801
|
-
fi
|
|
1802
|
-
|
|
1803
|
-
# Fallback: non-systemd environments (minimal containers without launchd).
|
|
1804
|
-
_ensure_nomad_hcl
|
|
1805
|
-
|
|
1806
|
-
ui_info "Starting Nomad agent..."
|
|
1807
|
-
local log_path="${nomad_config_dir}/nomad.log"
|
|
1808
|
-
# Run as the current (non-root) user — Nomad only binds to 127.0.0.1:4646/4647/4648
|
|
1809
|
-
# (non-privileged ports), and all data dirs live under ~/.jishushell/, so root is not needed.
|
|
1810
|
-
nohup "${nomad_bin}" agent -config="${config_file}" > "${log_path}" 2>&1 &
|
|
1811
|
-
|
|
1812
|
-
local i
|
|
1813
|
-
for i in $(seq 1 30); do
|
|
1814
|
-
sleep 1
|
|
1815
|
-
if _port_is_listening 4646; then
|
|
1816
|
-
ui_success "Nomad started"
|
|
1817
|
-
return 0
|
|
1818
|
-
fi
|
|
1819
|
-
done
|
|
1820
|
-
|
|
1821
|
-
ui_warn "Nomad did not start within 30s — port 4646 not listening"
|
|
1822
|
-
ui_info "Check log: ${log_path}"
|
|
1823
|
-
return 1
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
# Install Nomad as a system service (systemd on Linux, launchd on macOS)
|
|
1827
|
-
install_nomad_systemd() {
|
|
1828
|
-
local nomad_bin="${JISHUSHELL_BIN_DIR}/nomad"
|
|
1829
|
-
local nomad_config_dir="${JISHUSHELL_HOME}/nomad"
|
|
1830
|
-
local config_file="${nomad_config_dir}/nomad.hcl"
|
|
1831
|
-
|
|
1832
|
-
if [[ "$OS" == "macos" ]]; then
|
|
1833
|
-
_install_nomad_launchd
|
|
1834
|
-
return $?
|
|
1835
|
-
fi
|
|
1836
|
-
|
|
1837
|
-
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
1838
|
-
return 0
|
|
1839
|
-
fi
|
|
1840
|
-
|
|
1841
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1842
|
-
ui_info "[dry-run] Would install /etc/systemd/system/nomad.service"
|
|
1843
|
-
return 0
|
|
1844
|
-
fi
|
|
1845
|
-
|
|
1846
|
-
_ensure_nomad_hcl
|
|
1847
|
-
|
|
1848
|
-
local service_content="[Unit]
|
|
1849
|
-
Description=Nomad Agent
|
|
1850
|
-
After=network-online.target docker.service
|
|
1851
|
-
Wants=network-online.target
|
|
1852
|
-
|
|
1853
|
-
[Service]
|
|
1854
|
-
User=${REAL_USER}
|
|
1855
|
-
SupplementaryGroups=docker
|
|
1856
|
-
Type=simple
|
|
1857
|
-
EnvironmentFile=-/etc/jishushell/nomad.env
|
|
1858
|
-
ExecStart=${nomad_bin} agent -config=${config_file}
|
|
1859
|
-
Restart=on-failure
|
|
1860
|
-
RestartSec=3
|
|
1861
|
-
ProtectSystem=full
|
|
1862
|
-
PrivateTmp=true
|
|
1863
|
-
NoNewPrivileges=true
|
|
1864
|
-
|
|
1865
|
-
[Install]
|
|
1866
|
-
WantedBy=multi-user.target"
|
|
1867
|
-
|
|
1868
|
-
# Only update the service file if it changed (avoids unnecessary restarts)
|
|
1869
|
-
local svc_path="/etc/systemd/system/nomad.service"
|
|
1870
|
-
local need_reload=0
|
|
1871
|
-
if [[ ! -f "$svc_path" ]] || ! echo "$service_content" | diff -q - "$svc_path" &>/dev/null; then
|
|
1872
|
-
${SUDO} mkdir -p /etc/jishushell
|
|
1873
|
-
echo "$service_content" > /tmp/nomad.service.$$
|
|
1874
|
-
${SUDO} mv /tmp/nomad.service.$$ "$svc_path"
|
|
1875
|
-
need_reload=1
|
|
1876
|
-
fi
|
|
1877
|
-
|
|
1878
|
-
# Ensure Nomad data dirs are owned by the real user before the service starts
|
|
1879
|
-
chown -R "${REAL_USER}:${REAL_GID:-${REAL_USER}}" "${JISHUSHELL_HOME}" 2>/dev/null || true
|
|
1880
|
-
|
|
1881
|
-
if [[ $need_reload -eq 1 ]]; then
|
|
1882
|
-
${SUDO} systemctl daemon-reload
|
|
1883
|
-
fi
|
|
1884
|
-
|
|
1885
|
-
# Enable but do NOT start here — start_nomad() handles startup + waiting.
|
|
1886
|
-
# This avoids double-waiting (install_nomad_systemd 15s + start_nomad 30s).
|
|
1887
|
-
if ! systemctl is-enabled nomad &>/dev/null 2>&1; then
|
|
1888
|
-
${SUDO} systemctl enable nomad 2>/dev/null || \
|
|
1889
|
-
ui_warn "Could not enable nomad systemd service — Nomad may not auto-start on reboot"
|
|
1890
|
-
fi
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
_install_nomad_launchd() {
|
|
1894
|
-
local nomad_bin="${JISHUSHELL_BIN_DIR}/nomad"
|
|
1895
|
-
local nomad_config_dir="${JISHUSHELL_HOME}/nomad"
|
|
1896
|
-
local config_file="${nomad_config_dir}/nomad.hcl"
|
|
1897
|
-
local plist_label="com.jishushell.nomad"
|
|
1898
|
-
local plist_path="${HOME}/Library/LaunchAgents/${plist_label}.plist"
|
|
1899
|
-
local log_path="${nomad_config_dir}/nomad.log"
|
|
1900
|
-
|
|
1901
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1902
|
-
ui_info "[dry-run] Would install launchd agent: ${plist_path}"
|
|
1903
|
-
return 0
|
|
1904
|
-
fi
|
|
1905
|
-
|
|
1906
|
-
mkdir -p "${HOME}/Library/LaunchAgents"
|
|
1907
|
-
|
|
1908
|
-
local docker_sock="${HOME}/.docker/run/docker.sock"
|
|
1909
|
-
if [[ ! -S "$docker_sock" ]]; then
|
|
1910
|
-
docker_sock="/var/run/docker.sock"
|
|
1911
|
-
fi
|
|
1912
|
-
|
|
1913
|
-
cat > "$plist_path" << PLIST
|
|
1914
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1915
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1916
|
-
<plist version="1.0">
|
|
1917
|
-
<dict>
|
|
1918
|
-
<key>Label</key>
|
|
1919
|
-
<string>${plist_label}</string>
|
|
1920
|
-
<key>ProgramArguments</key>
|
|
1921
|
-
<array>
|
|
1922
|
-
<string>${nomad_bin}</string>
|
|
1923
|
-
<string>agent</string>
|
|
1924
|
-
<string>-config=${config_file}</string>
|
|
1925
|
-
</array>
|
|
1926
|
-
<key>EnvironmentVariables</key>
|
|
1927
|
-
<dict>
|
|
1928
|
-
<key>DOCKER_HOST</key>
|
|
1929
|
-
<string>unix://${docker_sock}</string>
|
|
1930
|
-
<key>PATH</key>
|
|
1931
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1932
|
-
</dict>
|
|
1933
|
-
<key>RunAtLoad</key>
|
|
1934
|
-
<true/>
|
|
1935
|
-
<key>KeepAlive</key>
|
|
1936
|
-
<true/>
|
|
1937
|
-
<key>StandardOutPath</key>
|
|
1938
|
-
<string>${log_path}</string>
|
|
1939
|
-
<key>StandardErrorPath</key>
|
|
1940
|
-
<string>${log_path}</string>
|
|
1941
|
-
</dict>
|
|
1942
|
-
</plist>
|
|
1943
|
-
PLIST
|
|
1944
|
-
|
|
1945
|
-
_ensure_nomad_hcl
|
|
1946
|
-
|
|
1947
|
-
launchctl unload "$plist_path" 2>/dev/null || true
|
|
1948
|
-
if launchctl load -w "$plist_path" 2>/dev/null; then
|
|
1949
|
-
ui_success "Nomad launchd agent installed and started"
|
|
1950
|
-
else
|
|
1951
|
-
ui_warn "Could not load nomad launchd agent"
|
|
1952
|
-
fi
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
# ─── 4. OpenClaw (docker pull official image) ─────────────────────────────────
|
|
1956
|
-
# For Docker mode: pull the official OpenClaw image from ghcr.io.
|
|
1957
|
-
# For non-Docker modes (process manager / raw_exec): npm install as before.
|
|
1958
|
-
|
|
1959
|
-
_save_openclaw_image_to_panel() {
|
|
1960
|
-
local image="$1"
|
|
1961
|
-
local panel_file="${JISHUSHELL_HOME}/panel.json"
|
|
1962
|
-
node -e "
|
|
1963
|
-
const fs = require('fs');
|
|
1964
|
-
const p = '${panel_file}';
|
|
1965
|
-
let cfg = {};
|
|
1966
|
-
try { cfg = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
|
|
1967
|
-
cfg.openclaw_image = '${image}';
|
|
1968
|
-
fs.mkdirSync(require('path').dirname(p), { recursive: true });
|
|
1969
|
-
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
1970
|
-
" 2>/dev/null || true
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
_read_openclaw_image_from_panel() {
|
|
1974
|
-
local panel_file="${JISHUSHELL_HOME}/panel.json"
|
|
1975
|
-
node -e "
|
|
1976
|
-
const fs = require('fs');
|
|
1977
|
-
const p = '${panel_file}';
|
|
1978
|
-
try {
|
|
1979
|
-
const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
1980
|
-
if (typeof cfg.openclaw_image === 'string' && cfg.openclaw_image.trim()) {
|
|
1981
|
-
process.stdout.write(cfg.openclaw_image.trim());
|
|
1982
|
-
}
|
|
1983
|
-
} catch {}
|
|
1984
|
-
" 2>/dev/null || true
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
# Install OpenClaw npm package on the host (for process manager / raw_exec modes).
|
|
1988
|
-
# Skipped when using official Docker image.
|
|
1989
|
-
_install_openclaw_npm() {
|
|
1990
|
-
local pkg_dir="${JISHUSHELL_HOME}/packages/openclaw"
|
|
1991
|
-
|
|
1992
|
-
local local_bin="${pkg_dir}/bin/openclaw"
|
|
1993
|
-
local local_pkg="${pkg_dir}/lib/node_modules/openclaw/package.json"
|
|
1994
|
-
local _need_install=0
|
|
1995
|
-
if [[ -f "$local_bin" ]]; then
|
|
1996
|
-
local current_ver
|
|
1997
|
-
current_ver="$(node -p "require('${local_pkg}').version" 2>/dev/null || echo "")"
|
|
1998
|
-
if [[ "${OPENCLAW_NPM_VERSION}" != "latest" && "${current_ver}" != "${OPENCLAW_NPM_VERSION}" ]]; then
|
|
1999
|
-
ui_info "OpenClaw installed version (v${current_ver:-unknown}) differs from requested (${OPENCLAW_NPM_VERSION}) — reinstalling..."
|
|
2000
|
-
_need_install=1
|
|
2001
|
-
else
|
|
2002
|
-
ui_success "OpenClaw npm package already installed: v${current_ver:-unknown}"
|
|
2003
|
-
fi
|
|
2004
|
-
else
|
|
2005
|
-
_need_install=1
|
|
2006
|
-
fi
|
|
2007
|
-
if [[ "$_need_install" == "1" ]]; then
|
|
2008
|
-
if ! command -v npm &>/dev/null; then
|
|
2009
|
-
ui_error "npm not found — install Node.js first"
|
|
2010
|
-
return 1
|
|
2011
|
-
fi
|
|
2012
|
-
ui_info "Installing OpenClaw npm package (openclaw@${OPENCLAW_NPM_VERSION})..."
|
|
2013
|
-
local _oc_registry=""
|
|
2014
|
-
if [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
2015
|
-
_oc_registry="--registry https://registry.npmmirror.com"
|
|
2016
|
-
ui_info "China network — using Alibaba npm registry (npmmirror.com)"
|
|
2017
|
-
fi
|
|
2018
|
-
mkdir -p "$pkg_dir"
|
|
2019
|
-
log_detail ""
|
|
2020
|
-
log_detail "[$(date '+%H:%M:%S')] npm install -g --prefix ${pkg_dir} openclaw@${OPENCLAW_NPM_VERSION}${_oc_registry:+ ${_oc_registry}}"
|
|
2021
|
-
# shellcheck disable=SC2086
|
|
2022
|
-
if ! retry_net "npm install -g --prefix openclaw@${OPENCLAW_NPM_VERSION}" 3 \
|
|
2023
|
-
log_cmd env -u npm_config_global -u npm_config_prefix -u npm_config_location \
|
|
2024
|
-
npm install -g --prefix "$pkg_dir" ${_oc_registry} "openclaw@${OPENCLAW_NPM_VERSION}"; then
|
|
2025
|
-
ui_error "Failed to install OpenClaw npm package"
|
|
2026
|
-
return 1
|
|
2027
|
-
fi
|
|
2028
|
-
fi
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
install_openclaw() {
|
|
2032
|
-
ui_stage "OpenClaw"
|
|
2033
|
-
|
|
2034
|
-
if [[ "${SKIP_OPENCLAW}" == "1" ]]; then
|
|
2035
|
-
ui_info "Skipped (--skip-openclaw / default)"
|
|
2036
|
-
return 0
|
|
2037
|
-
fi
|
|
2038
|
-
|
|
2039
|
-
if ! command -v docker &>/dev/null; then
|
|
2040
|
-
ui_error "Docker is not installed — cannot build OpenClaw image"
|
|
2041
|
-
return 1
|
|
2042
|
-
fi
|
|
2043
|
-
|
|
2044
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
2045
|
-
ui_info "[dry-run] Would: npm install -g --prefix openclaw@${OPENCLAW_NPM_VERSION}"
|
|
2046
|
-
ui_info "[dry-run] Would: docker build -t jishushell-openclaw:<version> (npm package + Python)"
|
|
2047
|
-
return 0
|
|
2048
|
-
fi
|
|
2049
|
-
|
|
2050
|
-
# ── Step 1: Install OpenClaw npm package (used as docker build context) ──
|
|
2051
|
-
_install_openclaw_npm || return 1
|
|
2052
|
-
|
|
2053
|
-
# Resolve versioned tag from installed package (e.g. jishushell-openclaw:2026.3.31)
|
|
2054
|
-
local pkg_dir="${JISHUSHELL_HOME}/packages/openclaw"
|
|
2055
|
-
local oc_ver
|
|
2056
|
-
oc_ver="$(node -p "require('${pkg_dir}/lib/node_modules/openclaw/package.json').version" 2>/dev/null || echo "")"
|
|
2057
|
-
local docker_tag="${OPENCLAW_DOCKER_TAG}"
|
|
2058
|
-
if [[ -n "$oc_ver" ]]; then
|
|
2059
|
-
docker_tag="jishushell-openclaw:${oc_ver}"
|
|
2060
|
-
fi
|
|
2061
|
-
local configured_tag=""
|
|
2062
|
-
|
|
2063
|
-
# ── Step 2: Ensure Docker daemon is accessible ────────────────────────────
|
|
2064
|
-
if ! docker_exec info &>/dev/null 2>&1; then
|
|
2065
|
-
if command -v sg &>/dev/null 2>/dev/null && sg docker -c "docker info" &>/dev/null 2>&1; then
|
|
2066
|
-
DOCKER_CMD_PREFIX="sg docker -c"
|
|
2067
|
-
ui_info "Docker group activated via 'sg docker'"
|
|
2068
|
-
elif ${SUDO} docker info &>/dev/null 2>&1; then
|
|
2069
|
-
DOCKER_USE_SUDO=1
|
|
2070
|
-
ui_info "Using 'sudo docker' (docker group not yet active in this shell)"
|
|
2071
|
-
else
|
|
2072
|
-
ui_warn "Docker daemon is not reachable"
|
|
2073
|
-
if [[ "$OS" == "macos" ]]; then
|
|
2074
|
-
ui_warn "Make sure Docker Desktop is running"
|
|
2075
|
-
else
|
|
2076
|
-
ui_warn "Ensure Docker is running: sudo systemctl start docker"
|
|
2077
|
-
fi
|
|
2078
|
-
return 1
|
|
2079
|
-
fi
|
|
2080
|
-
fi
|
|
2081
|
-
|
|
2082
|
-
# ── Step 3: Reuse the currently configured image when it already exists
|
|
2083
|
-
# locally. This keeps repeated installs idempotent and avoids rebuilding
|
|
2084
|
-
# when panel.json already points at a valid pinned image.
|
|
2085
|
-
configured_tag="$(_read_openclaw_image_from_panel)"
|
|
2086
|
-
if [[ -n "${configured_tag}" ]] && docker_exec image inspect "${configured_tag}" &>/dev/null 2>&1; then
|
|
2087
|
-
OPENCLAW_IMAGE="${configured_tag}"
|
|
2088
|
-
ui_success "Docker image ${configured_tag} already exists — reusing configured image"
|
|
2089
|
-
return 0
|
|
2090
|
-
fi
|
|
2091
|
-
|
|
2092
|
-
# ── Step 4: Skip if the requested build tag already exists ───────────────
|
|
2093
|
-
if docker_exec image inspect "${docker_tag}" &>/dev/null 2>&1; then
|
|
2094
|
-
OPENCLAW_IMAGE="${docker_tag}"
|
|
2095
|
-
_save_openclaw_image_to_panel "${docker_tag}"
|
|
2096
|
-
ui_success "Docker image ${docker_tag} already exists — skipping"
|
|
2097
|
-
return 0
|
|
2098
|
-
fi
|
|
2099
|
-
|
|
2100
|
-
# ── Step 5: Build Docker image (npm package + Python) ────────────────────
|
|
2101
|
-
local pkg_dir="${JISHUSHELL_HOME}/packages/openclaw"
|
|
2102
|
-
|
|
2103
|
-
# Write Dockerfile into the npm package directory (build context)
|
|
2104
|
-
# When NETWORK_CHINA=1, inject Alibaba apt mirror before apt-get update
|
|
2105
|
-
if [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
2106
|
-
ui_info "China network — injecting Alibaba apt mirror into Dockerfile"
|
|
2107
|
-
cat > "${pkg_dir}/Dockerfile" << 'DOCKERFILE'
|
|
2108
|
-
FROM node:22-slim
|
|
2109
|
-
USER root
|
|
2110
|
-
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g; s/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
|
|
2111
|
-
sed -i 's/deb.debian.org/mirrors.aliyun.com/g; s/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list 2>/dev/null || true
|
|
2112
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
2113
|
-
procps hostname curl git lsof openssl \
|
|
2114
|
-
python3 python3-pip python3-venv python3-dev && \
|
|
2115
|
-
ln -sf /usr/bin/python3 /usr/local/bin/python && \
|
|
2116
|
-
rm -rf /var/lib/apt/lists/*
|
|
2117
|
-
WORKDIR /app
|
|
2118
|
-
COPY --chown=node:node lib/node_modules/ ./node_modules/
|
|
2119
|
-
RUN ln -sf /app/node_modules/openclaw/openclaw.mjs /app/openclaw.mjs && \
|
|
2120
|
-
ln -sf /app/node_modules/openclaw/openclaw.mjs /usr/local/bin/openclaw && \
|
|
2121
|
-
cp /app/node_modules/openclaw/package.json /app/package.json 2>/dev/null || true
|
|
2122
|
-
USER node
|
|
2123
|
-
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
|
2124
|
-
DOCKERFILE
|
|
2125
|
-
else
|
|
2126
|
-
cat > "${pkg_dir}/Dockerfile" << 'DOCKERFILE'
|
|
2127
|
-
FROM node:22-slim
|
|
2128
|
-
USER root
|
|
2129
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
2130
|
-
procps hostname curl git lsof openssl \
|
|
2131
|
-
python3 python3-pip python3-venv python3-dev && \
|
|
2132
|
-
ln -sf /usr/bin/python3 /usr/local/bin/python && \
|
|
2133
|
-
rm -rf /var/lib/apt/lists/*
|
|
2134
|
-
WORKDIR /app
|
|
2135
|
-
COPY --chown=node:node lib/node_modules/ ./node_modules/
|
|
2136
|
-
RUN ln -sf /app/node_modules/openclaw/openclaw.mjs /app/openclaw.mjs && \
|
|
2137
|
-
ln -sf /app/node_modules/openclaw/openclaw.mjs /usr/local/bin/openclaw && \
|
|
2138
|
-
cp /app/node_modules/openclaw/package.json /app/package.json 2>/dev/null || true
|
|
2139
|
-
USER node
|
|
2140
|
-
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
|
2141
|
-
DOCKERFILE
|
|
2142
|
-
fi
|
|
2143
|
-
|
|
2144
|
-
ui_info "Building Docker image: ${docker_tag} (npm package + Python)..."
|
|
2145
|
-
log_detail ""
|
|
2146
|
-
log_detail "[$(date '+%H:%M:%S')] docker build -t ${docker_tag} ${pkg_dir}"
|
|
2147
|
-
if log_cmd docker_exec build --network=host -t "${docker_tag}" "${pkg_dir}"; then
|
|
2148
|
-
OPENCLAW_IMAGE="${docker_tag}"
|
|
2149
|
-
_save_openclaw_image_to_panel "${docker_tag}"
|
|
2150
|
-
local _local_tag="jishushell-openclaw:local"
|
|
2151
|
-
if [[ "${docker_tag}" != "${_local_tag}" ]]; then
|
|
2152
|
-
docker_exec tag "${docker_tag}" "${_local_tag}" 2>/dev/null || true
|
|
2153
|
-
fi
|
|
2154
|
-
rm -f "${pkg_dir}/Dockerfile"
|
|
2155
|
-
ui_success "OpenClaw Docker image built: ${docker_tag} (with Python)"
|
|
2156
|
-
else
|
|
2157
|
-
rm -f "${pkg_dir}/Dockerfile"
|
|
2158
|
-
ui_error "Failed to build OpenClaw Docker image"
|
|
2159
|
-
return 1
|
|
2160
|
-
fi
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
_prompt_openclaw_skip() {
|
|
2164
|
-
if [[ ! -t 0 || ! -t 1 ]]; then
|
|
2165
|
-
SKIP_OPENCLAW=0
|
|
2166
|
-
ui_info "Non-interactive shell detected — OpenClaw will be installed by default"
|
|
2167
|
-
return 0
|
|
2168
|
-
fi
|
|
2169
|
-
|
|
2170
|
-
local answer
|
|
2171
|
-
echo ""
|
|
2172
|
-
echo -e "${ACCENT}${BOLD}OpenClaw${NC}"
|
|
2173
|
-
echo -e "${INFO} Installs the OpenClaw npm package and builds a Docker image with Python.${NC}"
|
|
2174
|
-
echo -e "${INFO} Requires Docker to be running; the build may take a few minutes.${NC}"
|
|
2175
|
-
echo ""
|
|
2176
|
-
local answer answer_lc
|
|
2177
|
-
read -r -p "$(echo -e "${MUTED} Install OpenClaw and build Docker image? [Y/n]: ${NC}")" answer </dev/tty || answer="y"
|
|
2178
|
-
answer_lc="$(echo "$answer" | tr '[:upper:]' '[:lower:]')"
|
|
2179
|
-
case "$answer_lc" in
|
|
2180
|
-
n|no) SKIP_OPENCLAW=1 ;;
|
|
2181
|
-
*) SKIP_OPENCLAW=0 ;;
|
|
2182
|
-
esac
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
# show_install_plan [--with-jishushell]
|
|
2186
|
-
show_install_plan() {
|
|
2187
|
-
local with_jishushell=0
|
|
2188
|
-
[[ "${1:-}" == "--with-jishushell" ]] && with_jishushell=1
|
|
2189
|
-
|
|
2190
|
-
detect_network
|
|
2191
|
-
|
|
2192
|
-
echo ""
|
|
2193
|
-
echo -e "${ACCENT}${BOLD}Install Plan${NC}"
|
|
2194
|
-
echo -e "${MUTED}────────────────────────────────${NC}"
|
|
2195
|
-
ui_kv "OS" "$OS_NAME"
|
|
2196
|
-
ui_kv "Package manager" "$PKG_MANAGER"
|
|
2197
|
-
ui_kv "Architecture" "$ARCH"
|
|
2198
|
-
ui_kv "Network" "$(if [[ $NETWORK_CHINA -eq 1 ]]; then echo 'China (Alibaba mirrors)'; else echo 'Global'; fi)"
|
|
2199
|
-
echo ""
|
|
2200
|
-
ui_kv "Node.js" "$(if [[ $SKIP_NODE -eq 1 ]]; then echo 'skip'; else echo "v${NODE_VERSION} via nvm v${NVM_VERSION}"; fi)"
|
|
2201
|
-
ui_kv "Docker" "$(if [[ $SKIP_DOCKER -eq 1 ]]; then echo 'skip'; else echo 'latest stable'; fi)"
|
|
2202
|
-
ui_kv "Nomad" "$(if [[ $SKIP_NOMAD -eq 1 ]]; then echo 'skip'; else echo "v${NOMAD_VERSION}"; fi)"
|
|
2203
|
-
ui_kv "OpenClaw" "$(if [[ \"${SKIP_OPENCLAW}\" == \"1\" ]]; then echo 'skip'; else echo \"openclaw@${OPENCLAW_NPM_VERSION} + docker build ${OPENCLAW_DOCKER_TAG}\"; fi)"
|
|
2204
|
-
if [[ $with_jishushell -eq 1 ]]; then
|
|
2205
|
-
local _plan_jishu
|
|
2206
|
-
if [[ $SKIP_JISHUSHELL -eq 1 ]]; then
|
|
2207
|
-
_plan_jishu="skip"
|
|
2208
|
-
else
|
|
2209
|
-
local _plan_tgz=""
|
|
2210
|
-
for _c in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
|
|
2211
|
-
[[ -f "$_c" ]] && { _plan_tgz="$(basename "$_c")"; break; }
|
|
2212
|
-
done
|
|
2213
|
-
_plan_jishu="${_plan_tgz:+npm install -g ${_plan_tgz} (local)}${_plan_tgz:-npm install -g jishushell}"
|
|
2214
|
-
fi
|
|
2215
|
-
ui_kv "JishuShell" "$_plan_jishu"
|
|
2216
|
-
ui_kv "JishuShell service" "$(if [[ $SKIP_JISHUSHELL_SERVICE -eq 1 ]]; then echo 'skip'; else echo 'register autostart'; fi)"
|
|
2217
|
-
fi
|
|
2218
|
-
|
|
2219
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
2220
|
-
echo ""
|
|
2221
|
-
ui_warn "Dry-run mode: no changes will be made to this system"
|
|
2222
|
-
fi
|
|
2223
|
-
echo ""
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
# show_summary [--with-jishushell]
|
|
2227
|
-
show_summary() {
|
|
2228
|
-
local with_jishushell=0
|
|
2229
|
-
[[ "${1:-}" == "--with-jishushell" ]] && with_jishushell=1
|
|
2230
|
-
|
|
2231
|
-
echo ""
|
|
2232
|
-
echo -e "${ACCENT}${BOLD}Summary${NC}"
|
|
2233
|
-
echo -e "${MUTED}────────────────────────────────${NC}"
|
|
2234
|
-
|
|
2235
|
-
local all_ok=1
|
|
2236
|
-
|
|
2237
|
-
if [[ $SKIP_NODE -eq 0 ]]; then
|
|
2238
|
-
if command -v node &>/dev/null; then
|
|
2239
|
-
ui_kv "Node.js" "✓ $(node --version 2>/dev/null)"
|
|
2240
|
-
else
|
|
2241
|
-
ui_kv "Node.js" "✗ not installed"
|
|
2242
|
-
all_ok=0
|
|
2243
|
-
fi
|
|
2244
|
-
fi
|
|
2245
|
-
|
|
2246
|
-
if [[ $SKIP_DOCKER -eq 0 ]]; then
|
|
2247
|
-
if command -v docker &>/dev/null; then
|
|
2248
|
-
local _docker_ver
|
|
2249
|
-
_docker_ver="$(docker version --format '{{.Server.Version}}' 2>/dev/null | extract_semver || docker --version 2>/dev/null | extract_semver || echo 'installed')"
|
|
2250
|
-
ui_kv "Docker" "✓ v${_docker_ver}"
|
|
2251
|
-
else
|
|
2252
|
-
ui_kv "Docker" "✗ not installed"
|
|
2253
|
-
all_ok=0
|
|
2254
|
-
fi
|
|
2255
|
-
fi
|
|
2256
|
-
|
|
2257
|
-
if [[ $SKIP_NOMAD -eq 0 ]]; then
|
|
2258
|
-
local _nomad_bin="${JISHUSHELL_BIN_DIR}/nomad"
|
|
2259
|
-
if [[ -x "$_nomad_bin" ]]; then
|
|
2260
|
-
local _nomad_ver
|
|
2261
|
-
_nomad_ver="$("$_nomad_bin" version 2>/dev/null | head -n1 | extract_semver || echo 'installed')"
|
|
2262
|
-
ui_kv "Nomad" "✓ v${_nomad_ver} (${_nomad_bin})"
|
|
2263
|
-
elif command -v nomad &>/dev/null; then
|
|
2264
|
-
local _nomad_ver
|
|
2265
|
-
_nomad_ver="$(nomad version 2>/dev/null | head -n1 | extract_semver || echo 'installed')"
|
|
2266
|
-
ui_kv "Nomad" "✓ v${_nomad_ver} (system: $(command -v nomad))"
|
|
2267
|
-
else
|
|
2268
|
-
ui_kv "Nomad" "✗ not installed"
|
|
2269
|
-
all_ok=0
|
|
2270
|
-
fi
|
|
2271
|
-
fi
|
|
2272
|
-
|
|
2273
|
-
if [[ "${SKIP_OPENCLAW}" != "1" ]]; then
|
|
2274
|
-
if [[ -n "${OPENCLAW_IMAGE}" ]] && docker_exec image inspect "${OPENCLAW_IMAGE}" &>/dev/null 2>&1; then
|
|
2275
|
-
ui_kv "OpenClaw" "✓ ${OPENCLAW_IMAGE}"
|
|
2276
|
-
elif [[ "$DRY_RUN" == "1" ]]; then
|
|
2277
|
-
ui_kv "OpenClaw" "- dry-run"
|
|
2278
|
-
else
|
|
2279
|
-
ui_kv "OpenClaw" "✗ image not found locally"
|
|
2280
|
-
all_ok=0
|
|
2281
|
-
fi
|
|
2282
|
-
fi
|
|
2283
|
-
|
|
2284
|
-
if [[ $with_jishushell -eq 1 && $SKIP_JISHUSHELL -eq 0 ]]; then
|
|
2285
|
-
local _wrapper="${JISHUSHELL_BIN_DIR}/jishushell-panel-start"
|
|
2286
|
-
if [[ -x "$_wrapper" ]]; then
|
|
2287
|
-
ui_kv "JishuShell" "✓ ${_wrapper}"
|
|
2288
|
-
elif command -v jishushell &>/dev/null; then
|
|
2289
|
-
ui_kv "JishuShell" "✓ $(command -v jishushell)"
|
|
2290
|
-
else
|
|
2291
|
-
ui_kv "JishuShell" "✗ not found"
|
|
2292
|
-
all_ok=0
|
|
2293
|
-
fi
|
|
2294
|
-
fi
|
|
2295
|
-
|
|
2296
|
-
echo ""
|
|
2297
|
-
if [[ $all_ok -eq 1 ]]; then
|
|
2298
|
-
echo -e "${SUCCESS}${BOLD}All components installed successfully!${NC}"
|
|
2299
|
-
else
|
|
2300
|
-
echo -e "${WARN}${BOLD}One or more components failed — review the log above.${NC}"
|
|
2301
|
-
fi
|
|
2302
|
-
# Remind the user to re-login if docker group was added and sg could not
|
|
2303
|
-
# activate it in-session (DOCKER_CMD_PREFIX would be set if sg succeeded).
|
|
2304
|
-
if [[ "${DOCKER_GROUP_JUST_ADDED:-0}" == "1" && -z "${DOCKER_CMD_PREFIX:-}" ]]; then
|
|
2305
|
-
echo ""
|
|
2306
|
-
echo -e "${WARN}${BOLD}Action required — Docker group membership:${NC}"
|
|
2307
|
-
echo -e "${WARN} User '${REAL_USER:-$(id -un)}' was added to the 'docker' group, but the"
|
|
2308
|
-
echo -e "${WARN} change is not active in the current shell session.${NC}"
|
|
2309
|
-
echo -e "${WARN} Please log out and log back in, or run:${NC}"
|
|
2310
|
-
echo -e "${ACCENT} newgrp docker${NC}"
|
|
2311
|
-
echo -e "${WARN} to use Docker without sudo.${NC}"
|
|
2312
|
-
fi
|
|
2313
|
-
if [[ -n "${JISHU_LOG_FILE:-}" ]]; then
|
|
2314
|
-
echo ""
|
|
2315
|
-
ui_kv "Log file" "${JISHU_LOG_FILE}"
|
|
2316
|
-
fi
|
|
2317
|
-
echo ""
|
|
2318
|
-
if [[ $all_ok -eq 1 && $with_jishushell -eq 1 && $SKIP_JISHUSHELL -eq 0 ]]; then
|
|
2319
|
-
# Detect the primary non-loopback LAN IP address.
|
|
2320
|
-
# Use || true on every substitution to prevent set -e from firing when
|
|
2321
|
-
# the command does not exist on this OS (e.g. "ip" is Linux-only).
|
|
2322
|
-
local _local_ip=""
|
|
2323
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
2324
|
-
local _iface
|
|
2325
|
-
_iface="$(route -n get 1.1.1.1 2>/dev/null | awk '/interface:/{print $2}')" || true
|
|
2326
|
-
if [[ -n "$_iface" ]]; then
|
|
2327
|
-
_local_ip="$(ipconfig getifaddr "$_iface" 2>/dev/null)" || true
|
|
2328
|
-
fi
|
|
2329
|
-
if [[ -z "$_local_ip" ]]; then
|
|
2330
|
-
_local_ip="$(ipconfig getifaddr en0 2>/dev/null)" || true
|
|
2331
|
-
fi
|
|
2332
|
-
if [[ -z "$_local_ip" ]]; then
|
|
2333
|
-
_local_ip="$(ipconfig getifaddr en1 2>/dev/null)" || true
|
|
2334
|
-
fi
|
|
2335
|
-
else
|
|
2336
|
-
_local_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") {print $(i+1); exit}}')" || true
|
|
2337
|
-
if [[ -z "$_local_ip" ]]; then
|
|
2338
|
-
_local_ip="$(hostname -I 2>/dev/null | awk '{print $1}')" || true
|
|
2339
|
-
fi
|
|
2340
|
-
fi
|
|
2341
|
-
|
|
2342
|
-
echo -e "${SUCCESS}${BOLD} ╔══════════════════════════════════════════════════════╗${NC}"
|
|
2343
|
-
echo -e "${SUCCESS}${BOLD} ║ Installation complete! ║${NC}"
|
|
2344
|
-
echo -e "${SUCCESS}${BOLD} ║ ║${NC}"
|
|
2345
|
-
echo -e "${SUCCESS}${BOLD} ║ Open your browser and navigate to: ║${NC}"
|
|
2346
|
-
echo -e "${SUCCESS}${BOLD} ║ http://localhost:${JISHUSHELL_PORT}/$(printf '%*s' $((32 - ${#JISHUSHELL_PORT})) '')║${NC}"
|
|
2347
|
-
if [[ -n "$_local_ip" ]]; then
|
|
2348
|
-
echo -e "${SUCCESS}${BOLD} ║ http://${_local_ip}:${JISHUSHELL_PORT}/$(printf '%*s' $((28 - ${#_local_ip} - ${#JISHUSHELL_PORT})) '')║${NC}"
|
|
2349
|
-
fi
|
|
2350
|
-
echo -e "${SUCCESS}${BOLD} ╚══════════════════════════════════════════════════════╝${NC}"
|
|
2351
|
-
echo ""
|
|
2352
|
-
fi
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2356
|
-
# Core install orchestration
|
|
2357
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2358
|
-
|
|
2359
|
-
# ─── 6. JishuShell backend (npm install -g jishushell) ───────────────────────
|
|
2360
|
-
|
|
2361
|
-
install_jishushell() {
|
|
2362
|
-
ui_stage "JishuShell"
|
|
2363
|
-
|
|
2364
|
-
if [[ $SKIP_JISHUSHELL -eq 1 ]]; then
|
|
2365
|
-
ui_info "Skipped (--skip-jishushell)"
|
|
2366
|
-
return 0
|
|
2367
|
-
fi
|
|
2368
|
-
|
|
2369
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
2370
|
-
local _dry_reg=""
|
|
2371
|
-
[[ -n "${NPM_REGISTRY:-}" ]] && _dry_reg=" --registry ${NPM_REGISTRY}"
|
|
2372
|
-
local _dry_tgz=""
|
|
2373
|
-
for _c in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
|
|
2374
|
-
[[ -f "$_c" ]] && { _dry_tgz="$_c"; break; }
|
|
2375
|
-
done
|
|
2376
|
-
if [[ -n "$_dry_tgz" ]]; then
|
|
2377
|
-
ui_info "[dry-run] Would: npm install -g ${_dry_tgz} (local package)"
|
|
2378
|
-
else
|
|
2379
|
-
ui_info "[dry-run] Would: npm install -g jishushell${_dry_reg}"
|
|
2380
|
-
fi
|
|
2381
|
-
ui_info "[dry-run] Would write wrapper: ${JISHUSHELL_BIN_DIR}/jishushell-panel-start"
|
|
2382
|
-
return 0
|
|
2383
|
-
fi
|
|
2384
|
-
|
|
2385
|
-
local node_bin
|
|
2386
|
-
node_bin="$(command -v node 2>/dev/null || true)"
|
|
2387
|
-
# When running as root via sudo, node may only be in the real user's nvm.
|
|
2388
|
-
if [[ -z "$node_bin" && -n "${REAL_HOME}" ]]; then
|
|
2389
|
-
local nvm_node_dir="${REAL_HOME}/.nvm/versions/node"
|
|
2390
|
-
if [[ -d "$nvm_node_dir" ]]; then
|
|
2391
|
-
node_bin="$(find "$nvm_node_dir" -name node -type f 2>/dev/null | sort -V | tail -1 || true)"
|
|
2392
|
-
fi
|
|
2393
|
-
fi
|
|
2394
|
-
if [[ -z "$node_bin" ]]; then
|
|
2395
|
-
ui_error "Cannot locate node — wrapper cannot be written"
|
|
2396
|
-
return 1
|
|
2397
|
-
fi
|
|
2398
|
-
|
|
2399
|
-
# Locate npm sibling to the node binary (works with nvm-managed installs)
|
|
2400
|
-
local npm_bin
|
|
2401
|
-
npm_bin="$(dirname "$node_bin")/npm"
|
|
2402
|
-
if [[ ! -x "$npm_bin" ]]; then
|
|
2403
|
-
npm_bin="$(command -v npm 2>/dev/null || true)"
|
|
2404
|
-
fi
|
|
2405
|
-
if [[ -z "$npm_bin" ]]; then
|
|
2406
|
-
ui_error "Cannot locate npm — cannot install jishushell"
|
|
2407
|
-
return 1
|
|
2408
|
-
fi
|
|
2409
|
-
|
|
2410
|
-
local npm_registry_args=();
|
|
2411
|
-
if [[ -n "${NPM_REGISTRY:-}" ]]; then
|
|
2412
|
-
if [[ ! "$NPM_REGISTRY" =~ ^https?:// ]]; then
|
|
2413
|
-
ui_error "NPM_REGISTRY must be a valid URL starting with http:// or https://"
|
|
2414
|
-
return 1
|
|
2415
|
-
fi
|
|
2416
|
-
npm_registry_args=("--registry" "${NPM_REGISTRY}")
|
|
2417
|
-
ui_info "Installing jishushell from ${NPM_REGISTRY}..."
|
|
2418
|
-
elif [[ "${NETWORK_CHINA:-0}" == "1" ]]; then
|
|
2419
|
-
npm_registry_args=("--registry" "https://registry.npmmirror.com")
|
|
2420
|
-
ui_info "China network — installing jishushell from npmmirror.com..."
|
|
2421
|
-
else
|
|
2422
|
-
ui_info "Installing jishushell from public npm registry..."
|
|
2423
|
-
fi
|
|
2424
|
-
|
|
2425
|
-
# When jishushell is already installed (e.g. running as npm postinstall hook),
|
|
2426
|
-
# skip the npm install step and only write the wrapper + service.
|
|
2427
|
-
if [[ "${JISHUSHELL_SKIP_NPM_INSTALL:-0}" != "1" ]]; then
|
|
2428
|
-
# Prefer a local .tgz package in the same directory as this script.
|
|
2429
|
-
local tgz_path=""
|
|
2430
|
-
local _tgz_candidate
|
|
2431
|
-
for _tgz_candidate in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
|
|
2432
|
-
if [[ -f "$_tgz_candidate" ]]; then
|
|
2433
|
-
tgz_path="$_tgz_candidate"
|
|
2434
|
-
break
|
|
2435
|
-
fi
|
|
2436
|
-
done
|
|
2437
|
-
|
|
2438
|
-
# Export a sentinel so post-install.sh (triggered by npm's postinstall
|
|
2439
|
-
# lifecycle hook) knows it was launched from inside jishu-install.sh and
|
|
2440
|
-
# must not re-run Docker/Nomad/OpenClaw installation steps again.
|
|
2441
|
-
export JISHU_RUNNING_IN_INSTALLER=1
|
|
2442
|
-
|
|
2443
|
-
if [[ -n "$tgz_path" ]]; then
|
|
2444
|
-
ui_info "Found local package: ${tgz_path} — installing offline..."
|
|
2445
|
-
log_detail "[$(date '+%H:%M:%S')] ${npm_bin} install -g ${tgz_path}"
|
|
2446
|
-
if ! log_cmd "$npm_bin" install -g "${tgz_path}"; then
|
|
2447
|
-
unset JISHU_RUNNING_IN_INSTALLER
|
|
2448
|
-
ui_error "npm install -g ${tgz_path} failed"
|
|
2449
|
-
return 1
|
|
2450
|
-
fi
|
|
2451
|
-
else
|
|
2452
|
-
log_detail "[$(date '+%H:%M:%S')] ${npm_bin} install -g jishushell ${npm_registry_args[*]:-}"
|
|
2453
|
-
if ! log_cmd "$npm_bin" install -g jishushell ${npm_registry_args[@]+"${npm_registry_args[@]}"}; then
|
|
2454
|
-
unset JISHU_RUNNING_IN_INSTALLER
|
|
2455
|
-
ui_error "npm install -g jishushell failed"
|
|
2456
|
-
return 1
|
|
2457
|
-
fi
|
|
2458
|
-
fi
|
|
2459
|
-
unset JISHU_RUNNING_IN_INSTALLER
|
|
2460
|
-
else
|
|
2461
|
-
ui_info "Skipping npm install (already installed by caller)"
|
|
2462
|
-
fi
|
|
2463
|
-
|
|
2464
|
-
# Resolve the installed cli.js path via the npm that did the install
|
|
2465
|
-
local npm_root
|
|
2466
|
-
npm_root="$("$npm_bin" root -g 2>/dev/null || true)"
|
|
2467
|
-
if [[ -z "$npm_root" ]]; then
|
|
2468
|
-
ui_error "Cannot locate npm root — wrapper cannot be written"
|
|
2469
|
-
return 1
|
|
2470
|
-
fi
|
|
2471
|
-
local jishushell_bin="${npm_root}/jishushell/dist/cli.js"
|
|
2472
|
-
ui_success "JishuShell installed ($(${node_bin} -p "require('${npm_root}/jishushell/package.json').version" 2>/dev/null || echo 'version unknown'))"
|
|
2473
|
-
local wrapper="${JISHUSHELL_BIN_DIR}/jishushell-panel-start"
|
|
2474
|
-
mkdir -p "${JISHUSHELL_BIN_DIR}"
|
|
2475
|
-
|
|
2476
|
-
# The wrapper resolves node at runtime — it does NOT hardcode the path so
|
|
2477
|
-
# the binary stays valid if nvm is upgraded or node is reinstalled.
|
|
2478
|
-
cat > "$wrapper" << WRAPPER
|
|
2479
|
-
#!/usr/bin/env bash
|
|
2480
|
-
set -euo pipefail
|
|
2481
|
-
|
|
2482
|
-
_find_node() {
|
|
2483
|
-
command -v node 2>/dev/null && return
|
|
2484
|
-
local nvm_dir="\${NVM_DIR:-\${HOME}/.nvm}"
|
|
2485
|
-
[ -s "\$nvm_dir/nvm.sh" ] && . "\$nvm_dir/nvm.sh" --no-use 2>/dev/null || true
|
|
2486
|
-
command -v node 2>/dev/null && return
|
|
2487
|
-
local ndir="\${HOME}/.nvm/versions/node"
|
|
2488
|
-
[ -d "\$ndir" ] && find "\$ndir" -name node -type f 2>/dev/null | sort -V | tail -1 || true
|
|
2489
|
-
}
|
|
2490
|
-
NODE_BIN="\$(_find_node || true)"
|
|
2491
|
-
if [ -z "\$NODE_BIN" ]; then
|
|
2492
|
-
NODE_BIN="/opt/homebrew/bin/node"
|
|
2493
|
-
fi
|
|
2494
|
-
if [ ! -x "\$NODE_BIN" ]; then
|
|
2495
|
-
echo "[jishushell-panel-start] ERROR: cannot locate node binary" >&2
|
|
2496
|
-
exit 1
|
|
2497
|
-
fi
|
|
2498
|
-
|
|
2499
|
-
# Data directory: honour explicit env override, otherwise use the real
|
|
2500
|
-
# user home embedded at install time (avoids /root when run as root).
|
|
2501
|
-
JISHUSHELL_HOME="\${JISHUSHELL_HOME:-${REAL_HOME}/.jishushell}"
|
|
2502
|
-
NOMAD_ENV="\${JISHUSHELL_HOME}/nomad.env"
|
|
2503
|
-
|
|
2504
|
-
[ -f "\$NOMAD_ENV" ] && source "\$NOMAD_ENV"
|
|
2505
|
-
|
|
2506
|
-
if [ ! -f "${jishushell_bin}" ]; then
|
|
2507
|
-
echo "[jishushell-panel-start] ERROR: could not find jishushell at ${jishushell_bin}" >&2
|
|
2508
|
-
exit 1
|
|
2509
|
-
fi
|
|
2510
|
-
|
|
2511
|
-
exec "\$NODE_BIN" "${jishushell_bin}" "\$@"
|
|
2512
|
-
WRAPPER
|
|
2513
|
-
chmod +x "$wrapper"
|
|
2514
|
-
ui_success "JishuShell installed — wrapper: ${wrapper}"
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
install_jishushell_service() {
|
|
2518
|
-
ui_stage "JishuShell service"
|
|
2519
|
-
|
|
2520
|
-
if [[ $SKIP_JISHUSHELL_SERVICE -eq 1 ]]; then
|
|
2521
|
-
ui_info "Skipped (--skip-jishushell-service)"
|
|
2522
|
-
return 0
|
|
2523
|
-
fi
|
|
2524
|
-
|
|
2525
|
-
local wrapper="${JISHUSHELL_BIN_DIR}/jishushell-panel-start"
|
|
2526
|
-
local log_path="${JISHUSHELL_HOME}/jishushell.log"
|
|
2527
|
-
|
|
2528
|
-
if [[ "$DRY_RUN" == "1" ]]; then
|
|
2529
|
-
ui_info "[dry-run] Would register jishushell as a system service"
|
|
2530
|
-
return 0
|
|
2531
|
-
fi
|
|
2532
|
-
|
|
2533
|
-
if [[ "$OS" == "macos" ]]; then
|
|
2534
|
-
local plist_label="com.jishushell.panel"
|
|
2535
|
-
local plist_path="${HOME}/Library/LaunchAgents/${plist_label}.plist"
|
|
2536
|
-
|
|
2537
|
-
mkdir -p "${HOME}/Library/LaunchAgents"
|
|
2538
|
-
cat > "$plist_path" << PLIST
|
|
2539
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2540
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2541
|
-
<plist version="1.0">
|
|
2542
|
-
<dict>
|
|
2543
|
-
<key>Label</key>
|
|
2544
|
-
<string>${plist_label}</string>
|
|
2545
|
-
<key>ProgramArguments</key>
|
|
2546
|
-
<array>
|
|
2547
|
-
<string>${wrapper}</string>
|
|
2548
|
-
<string>serve</string>
|
|
2549
|
-
</array>
|
|
2550
|
-
<key>RunAtLoad</key>
|
|
2551
|
-
<true/>
|
|
2552
|
-
<key>KeepAlive</key>
|
|
2553
|
-
<true/>
|
|
2554
|
-
<key>StandardOutPath</key>
|
|
2555
|
-
<string>${log_path}</string>
|
|
2556
|
-
<key>StandardErrorPath</key>
|
|
2557
|
-
<string>${log_path}</string>
|
|
2558
|
-
</dict>
|
|
2559
|
-
</plist>
|
|
2560
|
-
PLIST
|
|
2561
|
-
|
|
2562
|
-
launchctl unload "$plist_path" 2>/dev/null || true
|
|
2563
|
-
if launchctl load -w "$plist_path" 2>/dev/null; then
|
|
2564
|
-
ui_success "JishuShell backend service installed and started"
|
|
2565
|
-
else
|
|
2566
|
-
ui_warn "Could not load jishushell launchd agent"
|
|
2567
|
-
fi
|
|
2568
|
-
return 0
|
|
2569
|
-
fi
|
|
2570
|
-
|
|
2571
|
-
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
2572
|
-
return 0
|
|
2573
|
-
fi
|
|
2574
|
-
|
|
2575
|
-
local service_content="[Unit]
|
|
2576
|
-
Description=JishuShell Backend
|
|
2577
|
-
After=network-online.target nomad.service
|
|
2578
|
-
Wants=network-online.target
|
|
2579
|
-
|
|
2580
|
-
[Service]
|
|
2581
|
-
Type=simple
|
|
2582
|
-
User=${REAL_USER}
|
|
2583
|
-
SupplementaryGroups=docker
|
|
2584
|
-
EnvironmentFile=-/etc/jishushell/nomad.env
|
|
2585
|
-
ExecStart=${wrapper} serve
|
|
2586
|
-
Restart=on-failure
|
|
2587
|
-
RestartSec=3
|
|
2588
|
-
Environment=HOME=${REAL_HOME}
|
|
2589
|
-
Environment=JISHUSHELL_HOME=${JISHUSHELL_HOME}
|
|
2590
|
-
ProtectSystem=strict
|
|
2591
|
-
PrivateTmp=true
|
|
2592
|
-
NoNewPrivileges=true
|
|
2593
|
-
ReadWritePaths=${JISHUSHELL_HOME} /etc/jishushell
|
|
2594
|
-
|
|
2595
|
-
[Install]
|
|
2596
|
-
WantedBy=multi-user.target"
|
|
2597
|
-
|
|
2598
|
-
# Only update the service file if it changed (avoids unnecessary restarts)
|
|
2599
|
-
local svc_path="/etc/systemd/system/jishushell.service"
|
|
2600
|
-
local need_reload=0
|
|
2601
|
-
if [[ ! -f "$svc_path" ]] || ! echo "$service_content" | diff -q - "$svc_path" &>/dev/null; then
|
|
2602
|
-
echo "$service_content" > /tmp/jishushell.service.$$
|
|
2603
|
-
${SUDO} mv /tmp/jishushell.service.$$ "$svc_path"
|
|
2604
|
-
need_reload=1
|
|
2605
|
-
fi
|
|
2606
|
-
|
|
2607
|
-
if [[ $need_reload -eq 1 ]]; then
|
|
2608
|
-
${SUDO} systemctl daemon-reload
|
|
2609
|
-
fi
|
|
2610
|
-
|
|
2611
|
-
# Enable the service (auto-start on boot).
|
|
2612
|
-
# Use 'enable --now' to also start it immediately — jishushell is the final
|
|
2613
|
-
# component so there is no double-wait concern unlike Nomad.
|
|
2614
|
-
if ! systemctl is-enabled jishushell &>/dev/null 2>&1; then
|
|
2615
|
-
if ${SUDO} systemctl enable --now jishushell 2>/dev/null; then
|
|
2616
|
-
ui_success "JishuShell systemd service installed and started"
|
|
2617
|
-
else
|
|
2618
|
-
ui_warn "Could not enable jishushell systemd service"
|
|
2619
|
-
fi
|
|
2620
|
-
elif [[ $need_reload -eq 1 ]]; then
|
|
2621
|
-
# Service file changed — restart to pick up new config
|
|
2622
|
-
${SUDO} systemctl restart jishushell 2>/dev/null || true
|
|
2623
|
-
ui_success "JishuShell systemd service updated and restarted"
|
|
2624
|
-
else
|
|
2625
|
-
ui_success "JishuShell systemd service already installed"
|
|
2626
|
-
fi
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
# ─── run_install_components ───────────────────────────────────────────────────
|
|
2630
|
-
# Runs the standard install sequence. Returns non-zero if any component fails.
|
|
2631
|
-
run_install_components() {
|
|
2632
|
-
local with_jishushell=0
|
|
2633
|
-
[[ "${1:-}" == "--with-jishushell" ]] && with_jishushell=1
|
|
2634
|
-
|
|
2635
|
-
local has_error=0
|
|
2636
|
-
|
|
2637
|
-
if [[ $SKIP_NODE -eq 0 ]]; then
|
|
2638
|
-
if ! install_node; then
|
|
2639
|
-
ui_error "Node.js installation failed"
|
|
2640
|
-
has_error=1
|
|
2641
|
-
fi
|
|
2642
|
-
else
|
|
2643
|
-
ui_stage "Node.js"
|
|
2644
|
-
ui_info "Skipped (--skip-node)"
|
|
2645
|
-
fi
|
|
2646
|
-
|
|
2647
|
-
local docker_ok=0
|
|
2648
|
-
if [[ $SKIP_DOCKER -eq 0 ]]; then
|
|
2649
|
-
if ! install_docker; then
|
|
2650
|
-
ui_error "Docker installation failed"
|
|
2651
|
-
has_error=1
|
|
2652
|
-
else
|
|
2653
|
-
docker_ok=1
|
|
2654
|
-
fi
|
|
2655
|
-
else
|
|
2656
|
-
ui_stage "Docker"
|
|
2657
|
-
ui_info "Skipped (--skip-docker)"
|
|
2658
|
-
command -v docker &>/dev/null && docker_ok=1
|
|
2659
|
-
fi
|
|
2660
|
-
|
|
2661
|
-
if [[ $SKIP_NOMAD -eq 0 ]]; then
|
|
2662
|
-
if ! install_nomad; then
|
|
2663
|
-
ui_error "Nomad installation failed"
|
|
2664
|
-
has_error=1
|
|
2665
|
-
else
|
|
2666
|
-
install_nomad_systemd || true
|
|
2667
|
-
# Ensure Nomad is actually running (mirrors install.ts startNomad() call).
|
|
2668
|
-
# start_nomad() is idempotent: on Linux it defers to systemd (no competing nohup);
|
|
2669
|
-
# on macOS/non-systemd it starts a background process.
|
|
2670
|
-
# A slow start (timeout) is non-fatal — systemd will retry on next boot and
|
|
2671
|
-
# 'jishushell doctor --fix' can recover it. Do not fail the entire npm install
|
|
2672
|
-
# just because Nomad took longer than expected to bind its port.
|
|
2673
|
-
if ! start_nomad; then
|
|
2674
|
-
ui_warn "Nomad could not be started — run 'jishushell doctor --fix' to diagnose"
|
|
2675
|
-
fi
|
|
2676
|
-
fi
|
|
2677
|
-
else
|
|
2678
|
-
ui_stage "Nomad"
|
|
2679
|
-
ui_info "Skipped (--skip-nomad)"
|
|
2680
|
-
fi
|
|
2681
|
-
|
|
2682
|
-
if [[ $docker_ok -eq 1 ]]; then
|
|
2683
|
-
if ! install_openclaw; then
|
|
2684
|
-
ui_error "OpenClaw installation failed"
|
|
2685
|
-
has_error=1
|
|
2686
|
-
fi
|
|
2687
|
-
else
|
|
2688
|
-
ui_stage "OpenClaw"
|
|
2689
|
-
ui_warn "Skipped — Docker is not available (installation failed or not installed)"
|
|
2690
|
-
fi
|
|
2691
|
-
|
|
2692
|
-
if [[ $with_jishushell -eq 1 ]]; then
|
|
2693
|
-
if [[ $SKIP_JISHUSHELL -eq 1 ]]; then
|
|
2694
|
-
ui_stage "JishuShell"
|
|
2695
|
-
ui_info "Skipped (--skip-jishushell)"
|
|
2696
|
-
elif ! install_jishushell; then
|
|
2697
|
-
ui_error "JishuShell installation failed"
|
|
2698
|
-
has_error=1
|
|
2699
|
-
fi
|
|
2700
|
-
|
|
2701
|
-
if [[ $SKIP_JISHUSHELL -eq 0 && $SKIP_JISHUSHELL_SERVICE -eq 1 ]]; then
|
|
2702
|
-
ui_stage "JishuShell service"
|
|
2703
|
-
ui_info "Skipped (--skip-jishushell-service)"
|
|
2704
|
-
elif [[ $SKIP_JISHUSHELL -eq 0 ]]; then
|
|
2705
|
-
install_jishushell_service || true
|
|
2706
|
-
fi
|
|
2707
|
-
fi
|
|
2708
|
-
|
|
2709
|
-
# ── Fix .jishushell ownership & permissions ───────────────────────────────
|
|
2710
|
-
# Ensure the data dir is readable/writable by both root and the real user.
|
|
2711
|
-
# - Root owns the dir (service runs as root)
|
|
2712
|
-
# - REAL_USER is in the owning group; dirs are g+rwx so normal-user tools work
|
|
2713
|
-
if [[ -d "${JISHUSHELL_HOME}" && -n "${REAL_USER}" ]]; then
|
|
2714
|
-
ui_info "Fixing ${JISHUSHELL_HOME} ownership for ${REAL_USER}..."
|
|
2715
|
-
# Both Nomad and JishuShell now run as REAL_USER — make REAL_USER own everything
|
|
2716
|
-
${SUDO} chown -R "${REAL_USER}:${REAL_GID:-${REAL_USER}}" "${JISHUSHELL_HOME}" 2>/dev/null || true
|
|
2717
|
-
# Dirs: rwxr-xr-x
|
|
2718
|
-
${SUDO} find "${JISHUSHELL_HOME}" -type d -exec chmod 755 {} + 2>/dev/null || true
|
|
2719
|
-
# Files: rw-r--r-- by default
|
|
2720
|
-
${SUDO} find "${JISHUSHELL_HOME}" -type f -exec chmod 644 {} + 2>/dev/null || true
|
|
2721
|
-
# Executables in bin/ must keep the execute bit
|
|
2722
|
-
${SUDO} find "${JISHUSHELL_BIN_DIR}" -type f -exec chmod 755 {} + 2>/dev/null || true
|
|
2723
|
-
# Sensitive files: owner-only read (600)
|
|
2724
|
-
for f in auth.json jwt-secret panel.json nomad.env encryption-key; do
|
|
2725
|
-
local fp="${JISHUSHELL_HOME}/${f}"
|
|
2726
|
-
[[ -f "$fp" ]] && ${SUDO} chmod 600 "$fp" 2>/dev/null || true
|
|
2727
|
-
done
|
|
2728
|
-
# Sync nomad.env from /etc/jishushell if missing in JISHUSHELL_HOME
|
|
2729
|
-
if [[ ! -f "${JISHUSHELL_HOME}/nomad.env" && -f /etc/jishushell/nomad.env ]]; then
|
|
2730
|
-
${SUDO} cp /etc/jishushell/nomad.env "${JISHUSHELL_HOME}/nomad.env"
|
|
2731
|
-
${SUDO} chown "${REAL_USER}:${REAL_GID:-${REAL_USER}}" "${JISHUSHELL_HOME}/nomad.env"
|
|
2732
|
-
${SUDO} chmod 600 "${JISHUSHELL_HOME}/nomad.env"
|
|
2733
|
-
fi
|
|
2734
|
-
ui_success "Permissions set: ${REAL_USER}:${REAL_USER} on ${JISHUSHELL_HOME}"
|
|
2735
|
-
fi
|
|
2736
|
-
|
|
2737
|
-
return $has_error
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2741
|
-
# Installer entry point — argument parsing, banner, main
|
|
2742
|
-
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2743
|
-
|
|
2744
|
-
parse_args() {
|
|
2745
|
-
while [[ $# -gt 0 ]]; do
|
|
2746
|
-
case "$1" in
|
|
2747
|
-
--verbose) VERBOSE=1 ;;
|
|
2748
|
-
--dry-run) DRY_RUN=1 ;;
|
|
2749
|
-
--skip-node) SKIP_NODE=1 ;;
|
|
2750
|
-
--skip-docker) SKIP_DOCKER=1 ;;
|
|
2751
|
-
--skip-nomad) SKIP_NOMAD=1 ;;
|
|
2752
|
-
--skip-openclaw) SKIP_OPENCLAW=1 ;;
|
|
2753
|
-
--openclaw-version)
|
|
2754
|
-
shift
|
|
2755
|
-
OPENCLAW_NPM_VERSION="${1:?--openclaw-version requires a version argument (e.g. 3.24)}"
|
|
2756
|
-
;;
|
|
2757
|
-
--skip-jishushell) SKIP_JISHUSHELL=1 ;;
|
|
2758
|
-
--skip-jishushell-service) SKIP_JISHUSHELL_SERVICE=1 ;;
|
|
2759
|
-
--skip)
|
|
2760
|
-
shift
|
|
2761
|
-
IFS=',' read -ra _steps <<< "${1:-}"
|
|
2762
|
-
for _s in "${_steps[@]}"; do
|
|
2763
|
-
case "$_s" in
|
|
2764
|
-
1) SKIP_NODE=1 ;;
|
|
2765
|
-
2) SKIP_DOCKER=1 ;;
|
|
2766
|
-
3) SKIP_NOMAD=1 ;;
|
|
2767
|
-
4) SKIP_OPENCLAW=1 ;;
|
|
2768
|
-
5) SKIP_JISHUSHELL=1 ;;
|
|
2769
|
-
6) SKIP_JISHUSHELL_SERVICE=1 ;;
|
|
2770
|
-
*) ui_warn "Unknown step number: $_s (valid: 1-6)" ;;
|
|
2771
|
-
esac
|
|
2772
|
-
done
|
|
2773
|
-
;;
|
|
2774
|
-
--run)
|
|
2775
|
-
# --run N,M → skip all steps NOT in the list
|
|
2776
|
-
# First set all to skip, then re-enable only the listed steps
|
|
2777
|
-
SKIP_NODE=1; SKIP_DOCKER=1; SKIP_NOMAD=1
|
|
2778
|
-
SKIP_OPENCLAW=1; SKIP_JISHUSHELL=1; SKIP_JISHUSHELL_SERVICE=1
|
|
2779
|
-
shift
|
|
2780
|
-
IFS=',' read -ra _steps <<< "${1:-}"
|
|
2781
|
-
for _s in "${_steps[@]}"; do
|
|
2782
|
-
case "$_s" in
|
|
2783
|
-
1) SKIP_NODE=0 ;;
|
|
2784
|
-
2) SKIP_DOCKER=0 ;;
|
|
2785
|
-
3) SKIP_NOMAD=0 ;;
|
|
2786
|
-
4) SKIP_OPENCLAW=0 ;;
|
|
2787
|
-
5) SKIP_JISHUSHELL=0 ;;
|
|
2788
|
-
6) SKIP_JISHUSHELL_SERVICE=0 ;;
|
|
2789
|
-
*) ui_warn "Unknown step number: $_s (valid: 1-6)" ;;
|
|
2790
|
-
esac
|
|
2791
|
-
done
|
|
2792
|
-
;;
|
|
2793
|
-
--registry)
|
|
2794
|
-
shift
|
|
2795
|
-
NPM_REGISTRY="${1:?--registry requires a URL argument}"
|
|
2796
|
-
if [[ ! "$NPM_REGISTRY" =~ ^https?:// ]]; then
|
|
2797
|
-
ui_error "--registry must be a valid URL starting with http:// or https://"
|
|
2798
|
-
exit 1
|
|
2799
|
-
fi
|
|
2800
|
-
;;
|
|
2801
|
-
--network)
|
|
2802
|
-
shift
|
|
2803
|
-
case "${1:-}" in
|
|
2804
|
-
china) NETWORK_CHINA=1 ;;
|
|
2805
|
-
global) NETWORK_CHINA=0 ;;
|
|
2806
|
-
*) ui_error "--network requires 'china' or 'global'"; exit 1 ;;
|
|
2807
|
-
esac
|
|
2808
|
-
NETWORK_OVERRIDE=1
|
|
2809
|
-
;;
|
|
2810
|
-
--yes|-y) AUTO_YES=1 ;;
|
|
2811
|
-
--help|-h) usage; exit 0 ;;
|
|
2812
|
-
*) ui_warn "Unknown argument: $1" ;;
|
|
2813
|
-
esac
|
|
2814
|
-
shift
|
|
2815
|
-
done
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
usage() {
|
|
2819
|
-
cat <<EOF
|
|
2820
|
-
Usage: bash jishu-install.sh [options]
|
|
2821
|
-
|
|
2822
|
-
Default (no options): runs steps 1,2,3,4,5,6
|
|
2823
|
-
|
|
2824
|
-
Options:
|
|
2825
|
-
--verbose Show verbose output
|
|
2826
|
-
--dry-run Show install plan only, do not execute
|
|
2827
|
-
--run <steps> Run only the specified steps (comma-separated, e.g. --run 1,2,3)
|
|
2828
|
-
--skip <steps> Skip steps by number (comma-separated, e.g. --skip 4)
|
|
2829
|
-
--skip-node Skip step 1: Node.js installation
|
|
2830
|
-
--skip-docker Skip step 2: Docker installation
|
|
2831
|
-
--skip-nomad Skip step 3: Nomad installation
|
|
2832
|
-
--skip-openclaw Skip step 4: OpenClaw installation
|
|
2833
|
-
--skip-jishushell Skip step 5: JishuShell installation
|
|
2834
|
-
--skip-jishushell-service Skip step 6: JishuShell service registration
|
|
2835
|
-
--registry <url> Use a custom npm registry for all installs
|
|
2836
|
-
(e.g. --registry http://10.188.0.22:4873/)
|
|
2837
|
-
--network <china|global> Force network mode (skips auto-detection via ping)
|
|
2838
|
-
china: use Alibaba mirrors for Docker and npm
|
|
2839
|
-
global: use default upstream sources
|
|
2840
|
-
--yes, -y Skip all confirmation prompts
|
|
2841
|
-
--help, -h Show this help message
|
|
2842
|
-
|
|
2843
|
-
Steps:
|
|
2844
|
-
1 Node.js (via nvm)
|
|
2845
|
-
2 Docker
|
|
2846
|
-
3 Nomad
|
|
2847
|
-
4 OpenClaw (npm install + docker build)
|
|
2848
|
-
5 JishuShell
|
|
2849
|
-
6 JishuShell service registration (autostart)
|
|
2850
|
-
|
|
2851
|
-
Environment variables:
|
|
2852
|
-
JISHU_NODE_VERSION Specify Node.js major version (default: ${NODE_VERSION})
|
|
2853
|
-
JISHU_NVM_VERSION Specify nvm version (default: ${NVM_VERSION})
|
|
2854
|
-
JISHU_NOMAD_VERSION Specify Nomad version (default: ${NOMAD_VERSION})
|
|
2855
|
-
OPENCLAW_NPM_VERSION Specify openclaw npm package version (default: latest)
|
|
2856
|
-
OPENCLAW_DOCKER_TAG Override built Docker image tag (default: jishushell-base:v1)
|
|
2857
|
-
NPM_REGISTRY Custom npm registry URL (same as --registry flag)
|
|
2858
|
-
|
|
2859
|
-
OpenClaw version flag (equivalent to OPENCLAW_NPM_VERSION env var):
|
|
2860
|
-
--openclaw-version <ver> Install a specific openclaw version, e.g. --openclaw-version 3.24
|
|
2861
|
-
NO_PROMPT Set to 1 to skip interactive prompts
|
|
2862
|
-
VERBOSE Set to 1 for verbose output
|
|
2863
|
-
|
|
2864
|
-
Examples:
|
|
2865
|
-
curl -fsSL https://www.aijishu.com/jishu-install.sh | bash
|
|
2866
|
-
NO_PROMPT=1 bash jishu-install.sh --dry-run
|
|
2867
|
-
EOF
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
print_banner() {
|
|
2871
|
-
echo -e "${ACCENT}${BOLD}"
|
|
2872
|
-
echo " ╔══════════════════════════════════════╗"
|
|
2873
|
-
echo " ║ JishuShell Installer ║"
|
|
2874
|
-
echo " ╚══════════════════════════════════════╝"
|
|
2875
|
-
echo -e "${NC}${INFO} ${TAGLINE}${NC}"
|
|
2876
|
-
echo ""
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
# ─── Install mode selection ───────────────────────────────────────────────────
|
|
2880
|
-
_prompt_install_confirm() {
|
|
2881
|
-
if [[ "${AUTO_YES:-0}" == "1" || "${NO_PROMPT:-0}" == "1" ]]; then
|
|
2882
|
-
return 0
|
|
2883
|
-
fi
|
|
2884
|
-
# Check /dev/tty directly — stdout/stdin may be redirected to the log FIFO
|
|
2885
|
-
# so the standard -t 0/-t 1 tests are unreliable here.
|
|
2886
|
-
if [[ ! -w /dev/tty || ! -r /dev/tty ]]; then
|
|
2887
|
-
AUTO_YES=1
|
|
2888
|
-
ui_info "Non-interactive shell detected — proceeding automatically"
|
|
2889
|
-
return 0
|
|
2890
|
-
fi
|
|
2891
|
-
|
|
2892
|
-
# Write everything directly to /dev/tty so output bypasses the log FIFO
|
|
2893
|
-
# and is guaranteed to appear on screen before `read` blocks for input.
|
|
2894
|
-
{
|
|
2895
|
-
echo -e "${ACCENT}${BOLD}┌─────────────────────────────────────────────────────────┐${NC}"
|
|
2896
|
-
echo -e "${ACCENT}${BOLD}│ THIRD-PARTY SOFTWARE NOTICE │${NC}"
|
|
2897
|
-
echo -e "${ACCENT}${BOLD}└─────────────────────────────────────────────────────────┘${NC}"
|
|
2898
|
-
echo ""
|
|
2899
|
-
echo -e " JishuShell assists in downloading and configuring the following"
|
|
2900
|
-
echo -e " third-party software packages for personal and internal use only."
|
|
2901
|
-
echo -e " Each package is governed solely by its own license. JishuShell"
|
|
2902
|
-
echo -e " does not modify, relicense, or assert ownership over any of them."
|
|
2903
|
-
echo ""
|
|
2904
|
-
echo -e " You are the end user of these packages. You are solely"
|
|
2905
|
-
echo -e " responsible for ensuring that your use of each package complies"
|
|
2906
|
-
echo -e " with its respective license terms, including any restrictions on"
|
|
2907
|
-
echo -e " commercial use, competitive offerings, or redistribution."
|
|
2908
|
-
echo ""
|
|
2909
|
-
|
|
2910
|
-
if [[ $SKIP_DOCKER -eq 0 ]]; then
|
|
2911
|
-
echo -e " ${BOLD}Docker Engine${NC}"
|
|
2912
|
-
echo -e " ${MUTED} URL : https://github.com/moby/moby${NC}"
|
|
2913
|
-
echo -e " ${MUTED} License : Apache License, Version 2.0${NC}"
|
|
2914
|
-
echo -e " ${MUTED} https://www.apache.org/licenses/LICENSE-2.0${NC}"
|
|
2915
|
-
echo -e " ${MUTED} Author : Docker, Inc.${NC}"
|
|
2916
|
-
echo ""
|
|
2917
|
-
fi
|
|
2918
|
-
if [[ $SKIP_NOMAD -eq 0 ]]; then
|
|
2919
|
-
echo -e " ${BOLD}Nomad v${NOMAD_VERSION}+ (>= 1.7.0)${NC}"
|
|
2920
|
-
echo -e " ${MUTED} URL : https://github.com/hashicorp/nomad${NC}"
|
|
2921
|
-
echo -e " ${MUTED} License : Business Source License 1.1 (BSL 1.1)${NC}"
|
|
2922
|
-
echo -e " ${MUTED} https://github.com/hashicorp/nomad/blob/main/LICENSE${NC}"
|
|
2923
|
-
echo -e " ${MUTED} Licensor: International Business Machines Corporation (IBM)${NC}"
|
|
2924
|
-
echo -e " ${MUTED} Work : Nomad Version 1.7.0 or later. (c) 2024 IBM Corp.${NC}"
|
|
2925
|
-
echo ""
|
|
2926
|
-
fi
|
|
2927
|
-
echo -e " ${MUTED}─────────────────────────────────────────────────────────${NC}"
|
|
2928
|
-
echo -e " ${MUTED}By continuing you acknowledge that you have read the above${NC}"
|
|
2929
|
-
echo -e " ${MUTED}notices and agree to each package's license terms.${NC}"
|
|
2930
|
-
echo -e " ${MUTED}sudo privileges are required to write to system directories.${NC}"
|
|
2931
|
-
echo ""
|
|
2932
|
-
} >/dev/tty
|
|
2933
|
-
|
|
2934
|
-
local answer
|
|
2935
|
-
read -r -p "$(echo -e " ${WARN}I have read and accept the above notices. Continue? [Y/n]: ${NC}")" answer </dev/tty || answer="y"
|
|
2936
|
-
case "$(echo "$answer" | tr '[:upper:]' '[:lower:]')" in
|
|
2937
|
-
n|no)
|
|
2938
|
-
echo "Installation cancelled." >/dev/tty
|
|
2939
|
-
exit 0
|
|
2940
|
-
;;
|
|
2941
|
-
esac
|
|
2942
|
-
echo "" >/dev/tty
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
# Finalize the install log: restore fds, wait for tee, strip ANSI codes.
|
|
2946
|
-
# Called from the EXIT trap so it runs on normal exit, Ctrl+C, and errors.
|
|
2947
|
-
_jishu_finalize_log() {
|
|
2948
|
-
set +e
|
|
2949
|
-
[[ -z "${_JISHU_RAW_LOG:-}" ]] && return 0 # not started or already finalized
|
|
2950
|
-
local _raw="${_JISHU_RAW_LOG}"
|
|
2951
|
-
local _log="${JISHU_LOG_FILE}"
|
|
2952
|
-
local _fifo="${_JISHU_LOG_FIFO:-}"
|
|
2953
|
-
local _tee_pid="${_JISHU_TEE_PID:-}"
|
|
2954
|
-
# Mark as done to prevent re-entry
|
|
2955
|
-
local _detail="${_JISHU_DETAIL_LOG:-}"
|
|
2956
|
-
_JISHU_RAW_LOG=""; _JISHU_TEE_PID=""; _JISHU_LOG_FIFO=""; _JISHU_DETAIL_LOG=""
|
|
2957
|
-
|
|
2958
|
-
# ── Step 1: kill the sudo keepalive background process ────────────────────
|
|
2959
|
-
# The keepalive is spawned before the FIFO redirect (so it does NOT hold
|
|
2960
|
-
# the FIFO write-end). We still kill it here for clean process accounting.
|
|
2961
|
-
if [[ -n "${_SUDO_KEEPALIVE_PID:-}" ]]; then
|
|
2962
|
-
kill "${_SUDO_KEEPALIVE_PID}" 2>/dev/null || true
|
|
2963
|
-
_SUDO_KEEPALIVE_PID=""
|
|
2964
|
-
fi
|
|
2965
|
-
|
|
2966
|
-
# ── Step 2: restore original fds ─────────────────────────────────────────
|
|
2967
|
-
# Closing our fd1 (the FIFO write-end) now sends EOF to tee because the
|
|
2968
|
-
# keepalive (the only other writer) is already dead.
|
|
2969
|
-
exec 1>&3 2>/dev/null
|
|
2970
|
-
exec 2>&4 2>/dev/null
|
|
2971
|
-
exec 3>&- 2>/dev/null
|
|
2972
|
-
exec 4>&- 2>/dev/null
|
|
2973
|
-
|
|
2974
|
-
# ── Step 3: wait for tee with a 5-second safety timeout ──────────────────
|
|
2975
|
-
if [[ -n "$_tee_pid" ]]; then
|
|
2976
|
-
local _n=0
|
|
2977
|
-
while kill -0 "$_tee_pid" 2>/dev/null && [[ $_n -lt 50 ]]; do
|
|
2978
|
-
sleep 0.1
|
|
2979
|
-
_n=$((_n + 1))
|
|
2980
|
-
done
|
|
2981
|
-
# Safety kill if tee is somehow still alive (e.g. another process held
|
|
2982
|
-
# the FIFO open), so this function never hangs the terminal.
|
|
2983
|
-
if kill -0 "$_tee_pid" 2>/dev/null; then
|
|
2984
|
-
kill "$_tee_pid" 2>/dev/null || true
|
|
2985
|
-
sleep 0.2
|
|
2986
|
-
kill -9 "$_tee_pid" 2>/dev/null || true
|
|
2987
|
-
fi
|
|
2988
|
-
fi
|
|
2989
|
-
|
|
2990
|
-
# ── Step 4: remove the FIFO ───────────────────────────────────────────────
|
|
2991
|
-
[[ -n "$_fifo" ]] && rm -f "$_fifo" 2>/dev/null || true
|
|
2992
|
-
|
|
2993
|
-
# ── Step 5: produce the clean log ────────────────────────────────────────
|
|
2994
|
-
# Strip ANSI escape codes AND decorative box-drawing characters
|
|
2995
|
-
# (╔══╗, ╚══╝, ════, ────, ── text ──) so the log is plain readable text.
|
|
2996
|
-
if [[ -f "${_raw}" ]]; then
|
|
2997
|
-
sed \
|
|
2998
|
-
-e 's/\x1B\[[0-9;]*[A-Za-z]//g' \
|
|
2999
|
-
-e 's/\r//g' \
|
|
3000
|
-
-e 's/[╔╗╚╝╠╣╦╩╬║│]//g' \
|
|
3001
|
-
-e 's/[═─]\{2,\}//g' \
|
|
3002
|
-
-e '/^[[:space:]]*$/d' \
|
|
3003
|
-
"${_raw}" > "${_log}" 2>/dev/null
|
|
3004
|
-
rm -f "${_raw}"
|
|
3005
|
-
fi
|
|
3006
|
-
# ── Step 6: append detailed command output ────────────────────────────────
|
|
3007
|
-
if [[ -f "${_detail}" && -s "${_detail}" ]]; then
|
|
3008
|
-
{
|
|
3009
|
-
printf '\n\n--- DETAILED COMMAND OUTPUT ---\n'
|
|
3010
|
-
cat "${_detail}"
|
|
3011
|
-
} >> "${_log}" 2>/dev/null
|
|
3012
|
-
rm -f "${_detail}"
|
|
3013
|
-
fi
|
|
3014
|
-
echo "Log saved to: ${_log}"
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
_jishu_install_main() {
|
|
3018
|
-
parse_args "$@"
|
|
3019
|
-
|
|
3020
|
-
# ── Set up log file ───────────────────────────────────────────────────────
|
|
3021
|
-
# Strategy:
|
|
3022
|
-
# 1. Create a named FIFO so we get an unambiguous tee PID via $!.
|
|
3023
|
-
# 2. Start tee with SIGINT/SIGTERM ignored — Ctrl+C won't kill it mid-write.
|
|
3024
|
-
# tee inherits the current (real terminal) stdout, so screen output is
|
|
3025
|
-
# preserved. Its stdin comes from the FIFO.
|
|
3026
|
-
# 3. Redirect all script output into the FIFO.
|
|
3027
|
-
# 4. EXIT trap calls _jishu_finalize_log which closes the FIFO write-end,
|
|
3028
|
-
# waits for tee with 'wait $PID', then strips ANSI codes to produce the
|
|
3029
|
-
# clean log. Works for normal exit, Ctrl+C, and set -e errors.
|
|
3030
|
-
local _log_ts
|
|
3031
|
-
_log_ts="$(date +%Y-%m-%d-%H-%M-%S)"
|
|
3032
|
-
JISHU_LOG_FILE="${JISHU_SCRIPT_DIR}/jishu-install-${_log_ts}.log"
|
|
3033
|
-
# Fall back to $PWD when the script directory is not writable (e.g. curl|bash).
|
|
3034
|
-
if [[ ! -w "${JISHU_SCRIPT_DIR}" ]]; then
|
|
3035
|
-
JISHU_LOG_FILE="${PWD}/jishu-install-${_log_ts}.log"
|
|
3036
|
-
fi
|
|
3037
|
-
_JISHU_RAW_LOG="${JISHU_LOG_FILE}.tmp"
|
|
3038
|
-
_JISHU_DETAIL_LOG="${JISHU_LOG_FILE}.detail"
|
|
3039
|
-
|
|
3040
|
-
# Create FIFO — $$ makes it unique per invocation, no mktemp race needed.
|
|
3041
|
-
_JISHU_LOG_FIFO="${TMPDIR:-/tmp}/jishu_log_$$.fifo"
|
|
3042
|
-
mkfifo "${_JISHU_LOG_FIFO}"
|
|
3043
|
-
|
|
3044
|
-
# Launch tee BEFORE redirecting so it inherits the real terminal as stdout.
|
|
3045
|
-
# trap '' INT TERM makes it immune to Ctrl+C and termination signals.
|
|
3046
|
-
( trap '' INT TERM; exec tee "${_JISHU_RAW_LOG}" ) < "${_JISHU_LOG_FIFO}" &
|
|
3047
|
-
_JISHU_TEE_PID=$!
|
|
3048
|
-
|
|
3049
|
-
# Override EXIT trap: finalize log first, then run normal temp-file cleanup.
|
|
3050
|
-
trap '_jishu_finalize_log; cleanup_tmpfiles' EXIT
|
|
3051
|
-
|
|
3052
|
-
# ── Pre-FIFO keepalive: must be spawned BEFORE the exec redirect below so
|
|
3053
|
-
# that it inherits the real terminal as fd 1 rather than the FIFO write-end.
|
|
3054
|
-
# A keepalive holding the FIFO open would prevent tee from ever seeing EOF
|
|
3055
|
-
# and cause _jishu_finalize_log to hang. Credentials are acquired later,
|
|
3056
|
-
# inside check_sudo() (which runs after the user confirms the install plan),
|
|
3057
|
-
# so the keepalive's early sudo -n true calls are intentional no-ops.
|
|
3058
|
-
if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then
|
|
3059
|
-
if [[ -z "${_SUDO_KEEPALIVE_PID:-}" ]]; then
|
|
3060
|
-
( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &
|
|
3061
|
-
_SUDO_KEEPALIVE_PID=$!
|
|
3062
|
-
disown "$_SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
|
3063
|
-
fi
|
|
3064
|
-
fi
|
|
3065
|
-
|
|
3066
|
-
# Save original terminal fds, then redirect everything into the FIFO.
|
|
3067
|
-
exec 3>&1 4>&2
|
|
3068
|
-
exec > "${_JISHU_LOG_FIFO}" 2>&1
|
|
3069
|
-
|
|
3070
|
-
print_banner
|
|
3071
|
-
ui_info "Logging to: ${JISHU_LOG_FILE}"
|
|
3072
|
-
detect_os
|
|
3073
|
-
detect_arch
|
|
3074
|
-
show_install_plan --with-jishushell
|
|
3075
|
-
if [[ "$OS" == "macos" ]]; then
|
|
3076
|
-
ui_warn "macOS is not supported yet — coming soon!"
|
|
3077
|
-
exit 0
|
|
3078
|
-
fi
|
|
3079
|
-
_prompt_install_confirm
|
|
3080
|
-
check_sudo
|
|
3081
|
-
ensure_prerequisites
|
|
3082
|
-
run_install_components --with-jishushell
|
|
3083
|
-
local rc=$?
|
|
3084
|
-
show_summary --with-jishushell
|
|
3085
|
-
exit $rc
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
# Only run main() when executed directly or piped via curl|bash (not when sourced).
|
|
3089
|
-
# When piped, BASH_SOURCE[0] is unset; when sourced it differs from $0.
|
|
3090
|
-
if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]] || [[ -z "${BASH_SOURCE[0]:-}" ]]; then
|
|
3091
|
-
_jishu_install_main "$@"
|
|
3092
|
-
fi
|