jishushell 0.4.2 → 0.4.17

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.
Files changed (136) hide show
  1. package/Dockerfile.openclaw-slim +58 -0
  2. package/INSTALL-NOTICE +45 -0
  3. package/dist/auth.js +3 -3
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli/app.d.ts +3 -0
  6. package/dist/cli/app.js +156 -0
  7. package/dist/cli/app.js.map +1 -0
  8. package/dist/{doctor.d.ts → cli/doctor.d.ts} +6 -1
  9. package/dist/{doctor.js → cli/doctor.js} +389 -27
  10. package/dist/cli/doctor.js.map +1 -0
  11. package/dist/cli/helpers.d.ts +4 -0
  12. package/dist/cli/helpers.js +32 -0
  13. package/dist/cli/helpers.js.map +1 -0
  14. package/dist/cli/job.d.ts +3 -0
  15. package/dist/cli/job.js +260 -0
  16. package/dist/cli/job.js.map +1 -0
  17. package/dist/cli/llm.d.ts +24 -0
  18. package/dist/cli/llm.js +593 -0
  19. package/dist/cli/llm.js.map +1 -0
  20. package/dist/cli/openclaw.d.ts +12 -0
  21. package/dist/cli/openclaw.js +156 -0
  22. package/dist/cli/openclaw.js.map +1 -0
  23. package/dist/cli/panel.d.ts +25 -0
  24. package/dist/cli/panel.js +734 -0
  25. package/dist/cli/panel.js.map +1 -0
  26. package/dist/cli.js +476 -219
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config.d.ts +22 -4
  29. package/dist/config.js +96 -55
  30. package/dist/config.js.map +1 -1
  31. package/dist/control.d.ts +13 -41
  32. package/dist/control.js +12 -1355
  33. package/dist/control.js.map +1 -1
  34. package/dist/install.d.ts +1 -1
  35. package/dist/install.js +15 -29
  36. package/dist/install.js.map +1 -1
  37. package/dist/routes/apps.d.ts +3 -0
  38. package/dist/routes/apps.js +99 -0
  39. package/dist/routes/apps.js.map +1 -0
  40. package/dist/routes/backup.d.ts +2 -0
  41. package/dist/routes/backup.js +370 -0
  42. package/dist/routes/backup.js.map +1 -0
  43. package/dist/routes/instances.d.ts +1 -0
  44. package/dist/routes/instances.js +61 -15
  45. package/dist/routes/instances.js.map +1 -1
  46. package/dist/routes/llm.d.ts +15 -0
  47. package/dist/routes/llm.js +246 -0
  48. package/dist/routes/llm.js.map +1 -0
  49. package/dist/routes/setup.js +32 -7
  50. package/dist/routes/setup.js.map +1 -1
  51. package/dist/routes/system.js +31 -6
  52. package/dist/routes/system.js.map +1 -1
  53. package/dist/server.js +69 -5
  54. package/dist/server.js.map +1 -1
  55. package/dist/services/app-compiler.d.ts +15 -0
  56. package/dist/services/app-compiler.js +169 -0
  57. package/dist/services/app-compiler.js.map +1 -0
  58. package/dist/services/app-manager.d.ts +17 -0
  59. package/dist/services/app-manager.js +168 -0
  60. package/dist/services/app-manager.js.map +1 -0
  61. package/dist/services/backup-manager.d.ts +253 -0
  62. package/dist/services/backup-manager.js +2014 -0
  63. package/dist/services/backup-manager.js.map +1 -0
  64. package/dist/services/backup-verify.d.ts +26 -0
  65. package/dist/services/backup-verify.js +240 -0
  66. package/dist/services/backup-verify.js.map +1 -0
  67. package/dist/services/instance-manager.d.ts +73 -5
  68. package/dist/services/instance-manager.js +446 -74
  69. package/dist/services/instance-manager.js.map +1 -1
  70. package/dist/services/job-manager.d.ts +22 -0
  71. package/dist/services/job-manager.js +102 -0
  72. package/dist/services/job-manager.js.map +1 -0
  73. package/dist/services/llm-proxy/adapters.js +5 -1
  74. package/dist/services/llm-proxy/adapters.js.map +1 -1
  75. package/dist/services/llm-proxy/index.d.ts +30 -0
  76. package/dist/services/llm-proxy/index.js +71 -1
  77. package/dist/services/llm-proxy/index.js.map +1 -1
  78. package/dist/services/llm-proxy/ssrf.js +1 -1
  79. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  80. package/dist/services/nomad-manager.js +263 -159
  81. package/dist/services/nomad-manager.js.map +1 -1
  82. package/dist/services/panel-manager.d.ts +40 -0
  83. package/dist/services/panel-manager.js +346 -0
  84. package/dist/services/panel-manager.js.map +1 -0
  85. package/dist/services/process-manager.js +24 -10
  86. package/dist/services/process-manager.js.map +1 -1
  87. package/dist/services/setup-manager.d.ts +4 -2
  88. package/dist/services/setup-manager.js +578 -154
  89. package/dist/services/setup-manager.js.map +1 -1
  90. package/dist/services/telemetry/activation.js +10 -7
  91. package/dist/services/telemetry/activation.js.map +1 -1
  92. package/dist/services/telemetry/client.js +7 -18
  93. package/dist/services/telemetry/client.js.map +1 -1
  94. package/dist/services/telemetry/heartbeat.js +12 -6
  95. package/dist/services/telemetry/heartbeat.js.map +1 -1
  96. package/dist/services/update-manager.d.ts +47 -0
  97. package/dist/services/update-manager.js +305 -0
  98. package/dist/services/update-manager.js.map +1 -0
  99. package/dist/types.d.ts +62 -0
  100. package/dist/utils/fs.d.ts +85 -0
  101. package/dist/utils/fs.js +111 -0
  102. package/dist/utils/fs.js.map +1 -0
  103. package/dist/utils/safe-json.d.ts +2 -0
  104. package/dist/utils/safe-json.js +22 -16
  105. package/dist/utils/safe-json.js.map +1 -1
  106. package/install/jishu-install.sh +582 -138
  107. package/install/jishu-uninstall.sh +276 -391
  108. package/install/post-install.sh +85 -3
  109. package/openclaw-entry.sh +15 -0
  110. package/package.json +12 -5
  111. package/public/assets/Dashboard-CQsp1Mr9.js +1 -0
  112. package/public/assets/InitPassword-BEC8SE4A.js +1 -0
  113. package/public/assets/InstanceDetail-B5wTgNEg.js +17 -0
  114. package/public/assets/{Login-RkjzTNWg.js → Login-D1Bt-Lyk.js} +1 -1
  115. package/public/assets/NewInstance-GQzm3K9D.js +1 -0
  116. package/public/assets/Settings-ByjGlqhP.js +1 -0
  117. package/public/assets/Setup-cMF21Y-8.js +1 -0
  118. package/public/assets/index-B6qQP4mH.css +1 -0
  119. package/public/assets/index-BuTQtuNy.js +16 -0
  120. package/public/assets/logo-black-theme-DywLAtFy.png +0 -0
  121. package/public/assets/logo-white-theme-DXffFAWw.png +0 -0
  122. package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
  123. package/public/assets/{usePolling-CqQ8hrNc.js → usePolling-CK0DfI4h.js} +1 -1
  124. package/public/assets/{vendor-i18n-Bvxxh8Di.js → vendor-i18n-CfW0RvgE.js} +1 -1
  125. package/public/assets/vendor-react-B1-3Yrt-.js +59 -0
  126. package/public/index.html +4 -4
  127. package/dist/doctor.js.map +0 -1
  128. package/public/assets/Dashboard-CAOQDYDR.js +0 -1
  129. package/public/assets/InitPassword-CkehIkJG.js +0 -1
  130. package/public/assets/InstanceDetail-CzW2S95J.js +0 -14
  131. package/public/assets/NewInstance-DdbErdjA.js +0 -1
  132. package/public/assets/Settings-BUD7zwv9.js +0 -1
  133. package/public/assets/Setup-RRTIERGG.js +0 -1
  134. package/public/assets/index-77Ug7feY.css +0 -1
  135. package/public/assets/index-DfRnVUQR.js +0 -16
  136. package/public/assets/vendor-react-DONn7uBV.js +0 -59
@@ -118,7 +118,11 @@ is_promptable() {
118
118
  if [[ "${NO_PROMPT:-0}" == "1" ]]; then
119
119
  return 1
120
120
  fi
121
- if [[ -r /dev/tty && -w /dev/tty ]]; then
121
+ # Web-triggered upgrades never have an interactive TTY
122
+ if [[ "${JISHUSHELL_WEB_UPDATE:-0}" == "1" ]]; then
123
+ return 1
124
+ fi
125
+ if ( : <> /dev/tty ) 2>/dev/null; then
122
126
  return 0
123
127
  fi
124
128
  return 1
@@ -161,7 +165,7 @@ JISHU_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd 2>/dev/null |
161
165
  # ──── BEGIN VERSIONS ────
162
166
  NODE_VERSION="${JISHU_NODE_VERSION:-22}"
163
167
  NVM_VERSION="${JISHU_NVM_VERSION:-0.40.4}"
164
- NOMAD_VERSION="${JISHU_NOMAD_VERSION:-1.11.3}"
168
+ NOMAD_VERSION="${JISHU_NOMAD_VERSION:-1.6.5}"
165
169
  JISHUSHELL_PORT="${JISHUSHELL_PORT:-8090}"
166
170
 
167
171
  # ──── NPM Registry Configuration ────
@@ -223,8 +227,13 @@ SKIP_NOMAD="${SKIP_NOMAD:-0}"
223
227
  SKIP_OPENCLAW="${SKIP_OPENCLAW:-0}" # default=0 (install); use --skip 4 or --skip-openclaw to skip
224
228
  SKIP_JISHUSHELL="${SKIP_JISHUSHELL:-0}" # 1=skip install_jishushell
225
229
  SKIP_JISHUSHELL_SERVICE="${SKIP_JISHUSHELL_SERVICE:-0}" # 1=skip service registration
230
+ JISHUSHELL_NPM_VERSION="${JISHUSHELL_NPM_VERSION:-latest}" # jishushell npm package version
231
+ JISHUSHELL_VERSION_OVERRIDE=0
232
+ if [[ "${JISHUSHELL_NPM_VERSION}" != "latest" ]]; then
233
+ JISHUSHELL_VERSION_OVERRIDE=1
234
+ fi
226
235
  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
236
+ OPENCLAW_DOCKER_TAG="${OPENCLAW_DOCKER_TAG:-ghcr.io/x-aijishu/openclaw-runtime:latest}" # pre-built image from registry
228
237
  OPENCLAW_IMAGE="" # set dynamically after pull/build
229
238
  AUTO_YES="${AUTO_YES:-0}"
230
239
  DOCKER_CMD_PREFIX="" # Set to "sg docker -c" when group activated via sg
@@ -405,18 +414,33 @@ detect_os() {
405
414
  ui_success "OS: ${OS_NAME} (package manager: ${PKG_MANAGER})"
406
415
  }
407
416
 
408
- # Detect CPU architecture. Sets: ARCH (amd64 | arm64)
417
+ # Detect CPU architecture. Sets: ARCH (arm64)
418
+ # Only Arm-family (aarch64, arm64, armv7l) and Apple Silicon (Darwin/arm64)
419
+ # are supported. x86_64, i686, riscv, mips, s390x, ppc, etc. are rejected.
409
420
  detect_arch() {
410
- ARCH="$(uname -m)"
411
- case "$ARCH" in
412
- x86_64|amd64) ARCH="amd64" ;;
413
- aarch64|arm64) ARCH="arm64" ;;
421
+ local raw_arch
422
+ raw_arch="$(uname -m)"
423
+ case "$raw_arch" in
424
+ aarch64|arm64)
425
+ ARCH="arm64"
426
+ ;;
427
+ armv7l|armv8l|armhf)
428
+ # 32-bit Arm — may work but not officially tested
429
+ ARCH="arm64"
430
+ ui_warn "32-bit Arm detected (${raw_arch}). 64-bit OS on a 64-bit board is strongly recommended."
431
+ ;;
414
432
  *)
415
- ui_error "Unsupported CPU architecture: $ARCH"
433
+ ui_error "Unsupported CPU architecture: ${raw_arch}"
434
+ ui_error ""
435
+ ui_error "JishuShell runs exclusively on Arm-based devices (aarch64 / arm64)."
436
+ ui_error "Supported examples: Raspberry Pi 4/5, Orange Pi 5, Jetson Orin,"
437
+ ui_error " Rockchip RK3588, Apple Silicon Mac (arm64 macOS)."
438
+ ui_error ""
439
+ ui_error "x86_64 / i686 / RISC-V / MIPS / s390x / PowerPC are not supported."
416
440
  exit 1
417
441
  ;;
418
442
  esac
419
- ui_success "Architecture: ${ARCH}"
443
+ ui_success "Architecture: ${ARCH} (${raw_arch})"
420
444
  }
421
445
 
422
446
  # Verify sudo access. Sets: SUDO ("" if root, "sudo" otherwise)
@@ -433,6 +457,10 @@ check_sudo() {
433
457
 
434
458
  if ! sudo -n true 2>/dev/null; then
435
459
  ui_info "Some steps require sudo — you may be prompted for your password."
460
+ if ! is_promptable; then
461
+ ui_error "Failed to obtain sudo privileges (no interactive TTY available)"
462
+ exit 1
463
+ fi
436
464
  if ! sudo -v </dev/tty; then
437
465
  ui_error "Failed to obtain sudo privileges"
438
466
  exit 1
@@ -446,7 +474,7 @@ check_sudo() {
446
474
  # This ensures 'sudo docker' works even after a long Docker install step
447
475
  # without prompting for the password again.
448
476
  if [[ -z "${_SUDO_KEEPALIVE_PID:-}" ]]; then
449
- ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &
477
+ ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &>/dev/null &
450
478
  _SUDO_KEEPALIVE_PID=$!
451
479
  disown "$_SUDO_KEEPALIVE_PID" 2>/dev/null || true
452
480
  fi
@@ -925,6 +953,41 @@ _ensure_nvm_shell_config() {
925
953
  install_docker() {
926
954
  ui_stage "Docker"
927
955
 
956
+ # ── macOS: use private Colima instance ─────────────────────────────────────
957
+ if [[ "$OS" == "macos" ]]; then
958
+ local need_brew=0
959
+ local need_profile=0
960
+
961
+ if ! command -v docker &>/dev/null || ! command -v colima &>/dev/null; then
962
+ need_brew=1
963
+ need_profile=1
964
+ elif ! _colima list 2>/dev/null | grep -q "${_COLIMA_PROFILE}"; then
965
+ need_profile=1
966
+ fi
967
+
968
+ if [[ $need_brew -eq 1 ]]; then
969
+ if ! _do_install_docker; then
970
+ ui_error "Colima installation failed"
971
+ return 1
972
+ fi
973
+ elif [[ $need_profile -eq 1 ]]; then
974
+ ui_info "Starting Colima VM (profile: ${_COLIMA_PROFILE})..."
975
+ mkdir -p "${_COLIMA_HOME}"
976
+ _colima start "${_COLIMA_PROFILE}" \
977
+ --vm-type vz --mount-type virtiofs --network-address \
978
+ --activate=false --cpu 2 --memory 4 --disk 60 >/dev/null \
979
+ || { ui_warn "colima start failed — run 'COLIMA_HOME=${_COLIMA_HOME} colima start ${_COLIMA_PROFILE}' manually"; return 1; }
980
+ export DOCKER_HOST="unix://${_COLIMA_SOCKET}"
981
+ ui_success "Colima is running"
982
+ else
983
+ ui_success "Docker and Colima already configured"
984
+ fi
985
+
986
+ _ensure_docker_running
987
+ return 0
988
+ fi
989
+
990
+ # ── Linux: standard Docker Engine ──────────────────────────────────────────
928
991
  local need_install_docker=0
929
992
  local need_install_compose=0
930
993
 
@@ -948,8 +1011,6 @@ install_docker() {
948
1011
  :
949
1012
  elif command -v docker-compose &>/dev/null; then
950
1013
  :
951
- elif [[ "$OS" == "macos" ]]; then
952
- :
953
1014
  else
954
1015
  need_install_compose=1
955
1016
  fi
@@ -962,8 +1023,6 @@ install_docker() {
962
1023
 
963
1024
  if [[ $need_install_docker -eq 1 ]]; then
964
1025
  ui_info "Docker not found — installing..."
965
- # _do_install_docker installs docker-compose-plugin in the same apt command,
966
- # so Compose V2 will be available immediately after — no separate step needed.
967
1026
  if ! _do_install_docker; then
968
1027
  ui_warn "Official Docker install script failed — trying system package manager fallback..."
969
1028
  if ! _do_install_docker_apt_fallback; then
@@ -971,7 +1030,6 @@ install_docker() {
971
1030
  return 1
972
1031
  fi
973
1032
  fi
974
- # Compose is bundled; skip the separate install step
975
1033
  need_install_compose=0
976
1034
  fi
977
1035
 
@@ -995,13 +1053,23 @@ _do_install_docker() {
995
1053
  fi
996
1054
 
997
1055
  if [[ "$OS" == "macos" ]]; then
998
- ui_warn "Automated Docker installation is not supported on macOS"
999
- ui_info "Please install Docker Desktop from https://www.docker.com/products/docker-desktop/"
1000
- if command -v brew &>/dev/null; then
1001
- ui_info "Or via Homebrew: brew install --cask docker"
1056
+ ui_info "Installing docker and colima via Homebrew..."
1057
+ if ! command -v brew &>/dev/null; then
1058
+ ui_warn "Homebrew not found. Install it from https://brew.sh then re-run this script."
1059
+ return 1
1002
1060
  fi
1003
- ui_info "After installation, open Docker Desktop and wait for the daemon to start, then re-run this script"
1004
- return 1
1061
+ brew install -q docker colima || { ui_warn "brew install failed"; return 1; }
1062
+ ui_success "docker and colima installed"
1063
+
1064
+ mkdir -p "${_COLIMA_HOME}"
1065
+ ui_info "Starting Colima VM (profile: ${_COLIMA_PROFILE})..."
1066
+ _colima start "${_COLIMA_PROFILE}" \
1067
+ --vm-type vz --mount-type virtiofs --network-address \
1068
+ --activate=false --cpu 2 --memory 4 --disk 60 >/dev/null \
1069
+ || { ui_warn "colima start failed — run 'COLIMA_HOME=${_COLIMA_HOME} colima start ${_COLIMA_PROFILE}' manually"; return 1; }
1070
+ ui_success "Colima is running"
1071
+ export DOCKER_HOST="unix://${_COLIMA_SOCKET}"
1072
+ return 0
1005
1073
  fi
1006
1074
 
1007
1075
  # Step 1: download
@@ -1096,7 +1164,7 @@ _do_install_docker_apt_fallback() {
1096
1164
  fi
1097
1165
 
1098
1166
  if [[ "$OS" == "macos" ]]; then
1099
- ui_warn "No apt/dnf fallback available on macOS — please install Docker Desktop manually"
1167
+ ui_warn "No apt/dnf fallback available on macOS — install via Homebrew: brew install docker colima"
1100
1168
  return 1
1101
1169
  fi
1102
1170
 
@@ -1157,17 +1225,30 @@ _ensure_docker_running() {
1157
1225
  fi
1158
1226
 
1159
1227
  if [[ "$OS" == "macos" ]]; then
1228
+ export DOCKER_HOST="unix://${_COLIMA_SOCKET}"
1229
+ if ! docker info &>/dev/null 2>&1; then
1230
+ ui_info "Starting Colima VM..."
1231
+ mkdir -p "${_COLIMA_HOME}"
1232
+ if ! _colima start "${_COLIMA_PROFILE}" \
1233
+ --vm-type vz --mount-type virtiofs --network-address \
1234
+ --activate=false --cpu 2 --memory 4 --disk 60 >/dev/null; then
1235
+ ui_warn "colima start failed"
1236
+ ui_info "Run manually: COLIMA_HOME=${_COLIMA_HOME} colima start ${_COLIMA_PROFILE}"
1237
+ return 1
1238
+ fi
1239
+ fi
1160
1240
  local waited=0
1241
+ local timeout=120
1161
1242
  while ! docker info &>/dev/null 2>&1; do
1162
- if [[ $waited -ge 15 ]]; then
1163
- ui_warn "Docker daemon did not become ready within 15 seconds"
1164
- ui_info "Make sure Docker Desktop is open and running"
1243
+ if [[ $waited -ge $timeout ]]; then
1244
+ ui_warn "Docker daemon did not become ready within ${timeout} seconds"
1245
+ ui_info "Run: COLIMA_HOME=${_COLIMA_HOME} colima status ${_COLIMA_PROFILE}"
1165
1246
  return 1
1166
1247
  fi
1167
- sleep 1
1168
- (( waited++ )) || true
1248
+ sleep 2
1249
+ (( waited += 2 )) || true
1169
1250
  done
1170
- [[ $waited -lt 15 ]] && ui_success "Docker daemon is ready"
1251
+ ui_success "Docker daemon is ready"
1171
1252
  return 0
1172
1253
  fi
1173
1254
 
@@ -1303,6 +1384,21 @@ docker_exec() {
1303
1384
  fi
1304
1385
  }
1305
1386
 
1387
+ # Private Colima wrapper — runs colima with COLIMA_HOME scoped to JishuShell's
1388
+ # data directory so the VM, socket, and all state are fully isolated from any
1389
+ # user-level Docker Desktop or default Colima installation.
1390
+ #
1391
+ # Usage: _colima start jishushell --vm-type vz ...
1392
+ # _colima stop jishushell
1393
+ # _colima status jishushell
1394
+ _COLIMA_HOME="${JISHUSHELL_HOME}/colima"
1395
+ _COLIMA_PROFILE="jishushell"
1396
+ _COLIMA_SOCKET="${_COLIMA_HOME}/${_COLIMA_PROFILE}/docker.sock"
1397
+
1398
+ _colima() {
1399
+ COLIMA_HOME="${_COLIMA_HOME}" command colima "$@"
1400
+ }
1401
+
1306
1402
  # ─── 3. Nomad ────────────────────────────────────────────────────────────────
1307
1403
 
1308
1404
  install_nomad() {
@@ -1330,12 +1426,24 @@ install_nomad() {
1330
1426
  if [[ -z "$current_version" ]]; then
1331
1427
  ui_warn "Nomad at ${local_bin} is not functional (wrong arch or corrupt) — reinstalling..."
1332
1428
  rm -f "$local_bin"
1429
+ elif [[ "$current_version" == "$NOMAD_VERSION" ]]; then
1430
+ ui_success "Nomad already at target version: v${current_version} → ${local_bin}"
1431
+ _ensure_jishushell_bin_in_path
1432
+ return 0
1333
1433
  elif version_gte "$current_version" "$NOMAD_VERSION"; then
1334
- ui_success "Nomad already installed: v${current_version} ${local_bin}"
1434
+ # current > target (the == case was handled above). JishuShell
1435
+ # pins Nomad to a specific version on purpose (license downgrade
1436
+ # from BSL 1.1 to MPL 2.0). Raft state is not backward compatible
1437
+ # across the jump, so we auto-migrate: download + verify the new
1438
+ # binary first (safe-first), then stop services, back up the old
1439
+ # data_dir, wipe it, clean orphaned containers, and swap the
1440
+ # binary. JishuShell re-bootstraps ACL and resubmits jobs from
1441
+ # on-disk instance configs on the next start.
1442
+ _migrate_nomad_to_target "$current_version" || return 1
1335
1443
  _ensure_jishushell_bin_in_path
1336
1444
  return 0
1337
1445
  else
1338
- ui_warn "Nomad version too old: v${current_version} (need >= v${NOMAD_VERSION}) — upgrading..."
1446
+ ui_warn "Nomad version too old: v${current_version} (need v${NOMAD_VERSION}) — upgrading..."
1339
1447
  rm -f "$local_bin"
1340
1448
  fi
1341
1449
  fi
@@ -1421,6 +1529,180 @@ REPO
1421
1529
  return 1
1422
1530
  }
1423
1531
 
1532
+ # Auto-migrate from a higher Nomad version (e.g. 1.11.3 BSL) back to the
1533
+ # jishushell target (1.6.5 MPL). Called when install_nomad detects a local
1534
+ # binary whose semver is > NOMAD_VERSION. The migration is destructive to
1535
+ # Nomad's raft state (schema is not backward compatible) but preserves
1536
+ # instance configs under ~/.jishushell/instances/*, which is what jishushell
1537
+ # uses to resubmit jobs after reboot. A single tar.gz snapshot of the old
1538
+ # data_dir is kept under ~/.jishushell/nomad/backups/ for forensic inspection
1539
+ # — it is not a user-recovery mechanism (the schema can't be replayed).
1540
+ _migrate_nomad_to_target() {
1541
+ local current_version="$1"
1542
+ local local_bin="${JISHUSHELL_BIN_DIR}/nomad"
1543
+
1544
+ if [[ "$DRY_RUN" == "1" ]]; then
1545
+ ui_info "[dry-run] Would migrate Nomad v${current_version} → v${NOMAD_VERSION}:"
1546
+ ui_info "[dry-run] 1. Stage + verify new binary in /tmp"
1547
+ ui_info "[dry-run] 2. Stop jishushell + nomad services"
1548
+ ui_info "[dry-run] 3. Tar backup ${JISHUSHELL_HOME}/nomad/data → nomad/backups/data-<ts>.tar.gz"
1549
+ ui_info "[dry-run] 4. Wipe raft state + nomad.env files (schema incompatible)"
1550
+ ui_info "[dry-run] 5. Remove orphaned gateway-<alloc> containers"
1551
+ ui_info "[dry-run] 6. Swap binary into ${local_bin}"
1552
+ return 0
1553
+ fi
1554
+
1555
+ ui_warn "Nomad v${current_version} > target v${NOMAD_VERSION} — auto-migrating (BSL → MPL)..."
1556
+ ui_info " Raft state is not backward-compatible; allocation history will be reset."
1557
+ ui_info " Instance configs under ${JISHUSHELL_HOME}/instances/ are preserved."
1558
+
1559
+ # ── Stage 1: download + verify new binary before touching anything ────
1560
+ local stage_dir
1561
+ stage_dir="$(mktemp -d)" || { ui_error "mktemp failed"; return 1; }
1562
+ # shellcheck disable=SC2064
1563
+ trap "rm -rf '$stage_dir'" RETURN
1564
+
1565
+ local platform
1566
+ platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
1567
+ local download_url="https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_${platform}_${ARCH}.zip"
1568
+ local sums_url="https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_SHA256SUMS"
1569
+
1570
+ ui_info "Staging Nomad v${NOMAD_VERSION} (${platform}/${ARCH})..."
1571
+ if ! retry_net "Download Nomad binary" 3 curl -fsSL "$download_url" -o "${stage_dir}/nomad.zip"; then
1572
+ ui_error "Failed to download Nomad v${NOMAD_VERSION} — keeping existing v${current_version}"
1573
+ return 1
1574
+ fi
1575
+ if ! retry_net "Download Nomad checksums" 3 curl -fsSL "$sums_url" -o "${stage_dir}/SHA256SUMS"; then
1576
+ ui_error "Failed to download Nomad checksum file — aborting migration for security"
1577
+ return 1
1578
+ fi
1579
+
1580
+ local expected_hash actual_hash
1581
+ expected_hash="$(grep "nomad_${NOMAD_VERSION}_${platform}_${ARCH}.zip" "${stage_dir}/SHA256SUMS" | awk '{print $1}')"
1582
+ if [[ -z "$expected_hash" ]]; then
1583
+ ui_error "No checksum entry for nomad_${NOMAD_VERSION}_${platform}_${ARCH}.zip — aborting"
1584
+ return 1
1585
+ fi
1586
+ if command -v sha256sum &>/dev/null; then
1587
+ actual_hash="$(sha256sum "${stage_dir}/nomad.zip" | awk '{print $1}')"
1588
+ else
1589
+ actual_hash="$(shasum -a 256 "${stage_dir}/nomad.zip" | awk '{print $1}')"
1590
+ fi
1591
+ if [[ "$expected_hash" != "$actual_hash" ]]; then
1592
+ ui_error "Nomad checksum mismatch — download may have been tampered with!"
1593
+ ui_error " Expected: $expected_hash"
1594
+ ui_error " Got: $actual_hash"
1595
+ return 1
1596
+ fi
1597
+ ui_info "Checksum verified ✓"
1598
+
1599
+ if ! command -v unzip &>/dev/null; then
1600
+ ui_info "Installing unzip..."
1601
+ pkg_install unzip >/dev/null 2>&1
1602
+ fi
1603
+ if ! unzip -o "${stage_dir}/nomad.zip" nomad -d "${stage_dir}" >/dev/null 2>&1; then
1604
+ if ! unzip -o "${stage_dir}/nomad.zip" -d "${stage_dir}" >/dev/null 2>&1; then
1605
+ ui_error "Failed to extract staged Nomad archive"
1606
+ return 1
1607
+ fi
1608
+ fi
1609
+ chmod 755 "${stage_dir}/nomad" 2>/dev/null
1610
+
1611
+ local staged_version
1612
+ staged_version="$("${stage_dir}/nomad" version 2>/dev/null | head -n1 | extract_semver || echo "")"
1613
+ if [[ "$staged_version" != "$NOMAD_VERSION" ]]; then
1614
+ ui_error "Staged binary reports v${staged_version:-unknown}, expected v${NOMAD_VERSION} — aborting"
1615
+ return 1
1616
+ fi
1617
+ ui_success "Staged new Nomad binary v${staged_version}"
1618
+
1619
+ # ── Stage 2: destructive state changes begin ──────────────────────────
1620
+ ui_info "Stopping services..."
1621
+ ${SUDO} systemctl stop jishushell 2>/dev/null || true
1622
+ ${SUDO} systemctl stop nomad 2>/dev/null || true
1623
+ # pkill -f 'nomad agent' matches its own cmdline ("pkill -f nomad agent"
1624
+ # literally contains the pattern) and self-terminates before reaching the
1625
+ # real nomad process. Use pgrep -x nomad instead (exact proc-name match,
1626
+ # pgrep's own comm is "pgrep" not "nomad").
1627
+ local nomad_pids
1628
+ nomad_pids="$(pgrep -x nomad 2>/dev/null | tr '\n' ' ')"
1629
+ if [[ -n "$nomad_pids" ]]; then
1630
+ # shellcheck disable=SC2086
1631
+ ${SUDO} kill -TERM $nomad_pids 2>/dev/null || kill -TERM $nomad_pids 2>/dev/null || true
1632
+ sleep 2
1633
+ nomad_pids="$(pgrep -x nomad 2>/dev/null | tr '\n' ' ')"
1634
+ if [[ -n "$nomad_pids" ]]; then
1635
+ # shellcheck disable=SC2086
1636
+ ${SUDO} kill -KILL $nomad_pids 2>/dev/null || kill -KILL $nomad_pids 2>/dev/null || true
1637
+ fi
1638
+ fi
1639
+
1640
+ # ── Stage 3: tar backup (single snapshot, overwrite any previous) ─────
1641
+ local backup_file=""
1642
+ local backup_dir="${JISHUSHELL_HOME}/nomad/backups"
1643
+ if [[ -d "${JISHUSHELL_HOME}/nomad/data" ]]; then
1644
+ mkdir -p "$backup_dir"
1645
+ local ts
1646
+ ts="$(date +%Y%m%d-%H%M%S)"
1647
+ backup_file="${backup_dir}/data-${ts}.tar.gz"
1648
+ ui_info "Backing up raft state → ${backup_file}"
1649
+ if ! tar czf "$backup_file" -C "${JISHUSHELL_HOME}/nomad" data 2>/dev/null; then
1650
+ ui_warn "Backup tar failed — continuing (raft state will still be wiped)"
1651
+ backup_file=""
1652
+ else
1653
+ # Keep only the most recent snapshot to avoid unbounded disk growth
1654
+ ls -t "${backup_dir}"/data-*.tar.gz 2>/dev/null | tail -n +2 | xargs -r rm -f
1655
+ fi
1656
+ fi
1657
+
1658
+ # ── Stage 4: wipe raft state + env files ─────────────────────────────
1659
+ ${SUDO} rm -rf "${JISHUSHELL_HOME}/nomad/data"
1660
+ rm -f "${JISHUSHELL_HOME}/nomad.env"
1661
+ ${SUDO} rm -f /etc/jishushell/nomad.env
1662
+
1663
+ # ── Stage 5: orphaned gateway containers (alloc ids gone with raft) ──
1664
+ # sudo npm install -g runs postinstall as the invoking user (typically pi),
1665
+ # whose login shell may not have docker group access — the legacy install
1666
+ # only granted docker to the nomad.service via SupplementaryGroups, not to
1667
+ # the login shell. Try unprivileged first, fall back to sudo docker so this
1668
+ # step works regardless of group membership.
1669
+ if command -v docker &>/dev/null; then
1670
+ local _docker="docker"
1671
+ if ! docker ps >/dev/null 2>&1; then
1672
+ _docker="${SUDO} docker"
1673
+ fi
1674
+ local gw_containers
1675
+ gw_containers="$($_docker ps -a --format '{{.Names}}' 2>/dev/null | grep '^gateway-' || true)"
1676
+ if [[ -n "$gw_containers" ]]; then
1677
+ local gw_count
1678
+ gw_count="$(echo "$gw_containers" | wc -l)"
1679
+ echo "$gw_containers" | xargs -r $_docker rm -f >/dev/null 2>&1 || true
1680
+ ui_info "Removed ${gw_count} orphaned gateway container(s)"
1681
+ fi
1682
+ fi
1683
+
1684
+ # ── Stage 6: swap binary into place (atomic via temp name + rename) ──
1685
+ mkdir -p "${JISHUSHELL_BIN_DIR}"
1686
+ local dest_tmp="${local_bin}.tmp.$$"
1687
+ if ! cp "${stage_dir}/nomad" "$dest_tmp"; then
1688
+ ui_error "Failed to copy new Nomad binary into place"
1689
+ [[ -n "$backup_file" ]] && ui_error " Backup preserved at: ${backup_file}"
1690
+ return 1
1691
+ fi
1692
+ chmod 755 "$dest_tmp"
1693
+ if ! mv -f "$dest_tmp" "$local_bin"; then
1694
+ ui_error "Failed to swap Nomad binary"
1695
+ [[ -n "$backup_file" ]] && ui_error " Backup preserved at: ${backup_file}"
1696
+ rm -f "$dest_tmp"
1697
+ return 1
1698
+ fi
1699
+
1700
+ ui_success "Nomad migrated to v${NOMAD_VERSION}"
1701
+ [[ -n "$backup_file" ]] && ui_info " Backup (forensic, not self-recovery): ${backup_file}"
1702
+ ui_info " JishuShell will re-bootstrap ACL and resubmit jobs from instance configs on next start."
1703
+ return 0
1704
+ }
1705
+
1424
1706
  _install_nomad_binary() {
1425
1707
  local dest="${JISHUSHELL_BIN_DIR}/nomad"
1426
1708
 
@@ -1531,17 +1813,29 @@ _install_nomad_binary() {
1531
1813
  ui_success "Nomad installed: v${installed_version} → ${dest}"
1532
1814
  }
1533
1815
 
1534
- # Add ~/.jishushell/bin to PATH in shell startup files and current session
1816
+ # Add ~/.jishushell/bin and npm global bin to PATH in shell startup files and current session
1535
1817
  _ensure_jishushell_bin_in_path() {
1536
1818
  local bin_dir="${JISHUSHELL_BIN_DIR}"
1537
1819
  local marker="# jishushell-bin-path"
1820
+
1821
+ # Also ensure npm global bin is in PATH (for `npm install -g` with custom prefix)
1822
+ local npm_bin=""
1823
+ if command -v npm &>/dev/null; then
1824
+ npm_bin="$(npm config get prefix 2>/dev/null)/bin"
1825
+ fi
1826
+
1827
+ # Build PATH line: include npm global bin if it differs from jishushell bin
1538
1828
  local init_line="export PATH=\"${bin_dir}:\$PATH\""
1829
+ if [[ -n "$npm_bin" && "$npm_bin" != "$bin_dir" && -d "$npm_bin" ]]; then
1830
+ init_line="export PATH=\"${bin_dir}:${npm_bin}:\$PATH\""
1831
+ export PATH="${npm_bin}:${PATH}"
1832
+ fi
1539
1833
 
1540
1834
  # Export for the current running shell immediately
1541
1835
  export PATH="${bin_dir}:${PATH}"
1542
1836
 
1543
1837
  if [[ "$DRY_RUN" == "1" ]]; then
1544
- ui_info "[dry-run] Would add ${bin_dir} to PATH in shell startup files"
1838
+ ui_info "[dry-run] Would add PATH entries in shell startup files"
1545
1839
  return 0
1546
1840
  fi
1547
1841
 
@@ -1550,7 +1844,7 @@ _ensure_jishushell_bin_in_path() {
1550
1844
  for rc in "${rc_files[@]}"; do
1551
1845
  if [[ -f "$rc" ]] && ! grep -qF "$marker" "$rc" 2>/dev/null; then
1552
1846
  printf '\n%s\n%s\n' "$marker" "$init_line" >> "$rc"
1553
- ui_info "Added ${bin_dir} to PATH in ${rc}"
1847
+ ui_info "Added PATH entries in ${rc}"
1554
1848
  added=1
1555
1849
  fi
1556
1850
  done
@@ -1587,6 +1881,14 @@ _ensure_nomad_hcl() {
1587
1881
  # Dirs are created by the current user — no sudo needed
1588
1882
  chown -R "${REAL_USER}:${REAL_GID:-${REAL_USER}}" "${JISHUSHELL_HOME}" 2>/dev/null || true
1589
1883
 
1884
+ # Loopback interface name: lo0 on macOS, lo on Linux.
1885
+ # Forces Nomad to fingerprint 127.0.0.1 as the node IP so Docker port
1886
+ # publishing binds to loopback instead of the LAN IP. On macOS+Colima the
1887
+ # LAN IP doesn't exist inside the Lima VM, causing "cannot assign requested
1888
+ # address" when Docker tries to bind to it.
1889
+ local loopback_iface="lo"
1890
+ [[ "$OS" == "macos" ]] && loopback_iface="lo0"
1891
+
1590
1892
  cat > "$config_file" << NOMAD_HCL
1591
1893
  data_dir = "${nomad_data_dir}"
1592
1894
 
@@ -1608,6 +1910,7 @@ server {
1608
1910
  client {
1609
1911
  enabled = true
1610
1912
  servers = ["127.0.0.1:4647"]
1913
+ network_interface = "${loopback_iface}"
1611
1914
  alloc_dir = "${nomad_alloc_dir}"
1612
1915
 
1613
1916
  drain_on_shutdown {
@@ -1745,14 +2048,17 @@ install_nomad_systemd() {
1745
2048
 
1746
2049
  _ensure_nomad_hcl
1747
2050
 
2051
+ # Nomad 1.6.5's docker driver fingerprint requires euid==0 (PR #18197 lifted
2052
+ # the root requirement only in 1.7+, which is BSL). The panel stays as the
2053
+ # installing user via a separate unit; it talks to this agent over HTTP so
2054
+ # ~/.jishushell/nomad/data/ can be root-owned without breaking anything.
1748
2055
  local service_content="[Unit]
1749
2056
  Description=Nomad Agent
1750
2057
  After=network-online.target docker.service
1751
2058
  Wants=network-online.target
1752
2059
 
1753
2060
  [Service]
1754
- User=${REAL_USER}
1755
- SupplementaryGroups=docker
2061
+ User=root
1756
2062
  Type=simple
1757
2063
  EnvironmentFile=-/etc/jishushell/nomad.env
1758
2064
  ExecStart=${nomad_bin} agent -config=${config_file}
@@ -1775,8 +2081,12 @@ WantedBy=multi-user.target"
1775
2081
  need_reload=1
1776
2082
  fi
1777
2083
 
1778
- # Ensure Nomad data dirs are owned by the real user before the service starts
2084
+ # Keep the real user owning ~/.jishushell except for Nomad's own state,
2085
+ # which must be root-owned because the agent runs as root for driver fingerprinting.
1779
2086
  chown -R "${REAL_USER}:${REAL_GID:-${REAL_USER}}" "${JISHUSHELL_HOME}" 2>/dev/null || true
2087
+ if [[ -d "${nomad_config_dir}/data" ]]; then
2088
+ ${SUDO} chown -R root:root "${nomad_config_dir}/data" 2>/dev/null || true
2089
+ fi
1780
2090
 
1781
2091
  if [[ $need_reload -eq 1 ]]; then
1782
2092
  ${SUDO} systemctl daemon-reload
@@ -1805,10 +2115,10 @@ _install_nomad_launchd() {
1805
2115
 
1806
2116
  mkdir -p "${HOME}/Library/LaunchAgents"
1807
2117
 
1808
- local docker_sock="${HOME}/.docker/run/docker.sock"
1809
- if [[ ! -S "$docker_sock" ]]; then
1810
- docker_sock="/var/run/docker.sock"
1811
- fi
2118
+ # Always use JishuShell's private Colima socket — hardcoded, not runtime-detected.
2119
+ # Colima may not be running yet when the plist is written; runtime fallback would
2120
+ # pick the wrong socket (Docker Desktop or /var/run/docker.sock).
2121
+ local docker_sock="${_COLIMA_SOCKET}"
1812
2122
 
1813
2123
  cat > "$plist_path" << PLIST
1814
2124
  <?xml version="1.0" encoding="UTF-8"?>
@@ -1870,6 +2180,54 @@ fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
1870
2180
  " 2>/dev/null || true
1871
2181
  }
1872
2182
 
2183
+ _read_openclaw_image_from_panel() {
2184
+ local panel_file="${JISHUSHELL_HOME}/panel.json"
2185
+ node -e "
2186
+ const fs = require('fs');
2187
+ const p = '${panel_file}';
2188
+ try {
2189
+ const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
2190
+ if (typeof cfg.openclaw_image === 'string' && cfg.openclaw_image.trim()) {
2191
+ process.stdout.write(cfg.openclaw_image.trim());
2192
+ }
2193
+ } catch {}
2194
+ " 2>/dev/null || true
2195
+ }
2196
+
2197
+ _pin_openclaw_image_if_needed() {
2198
+ local image="$1"
2199
+ if [[ -z "${image}" ]]; then
2200
+ return 1
2201
+ fi
2202
+ if [[ ! "${image}" =~ :(latest|slim)$ ]]; then
2203
+ printf '%s' "${image}"
2204
+ return 0
2205
+ fi
2206
+
2207
+ local version=""
2208
+ version="$(docker_exec run --rm --entrypoint node "${image}" -p "require('/app/node_modules/openclaw/package.json').version" 2>/dev/null | tr -d '\r\n')"
2209
+ if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
2210
+ printf '%s' "${image}"
2211
+ return 0
2212
+ fi
2213
+
2214
+ local repo="${image%:*}"
2215
+ local pinned="${repo}:${version}"
2216
+ if docker_exec image inspect "${pinned}" &>/dev/null 2>&1; then
2217
+ printf '%s' "${pinned}"
2218
+ return 0
2219
+ fi
2220
+
2221
+ if docker_exec tag "${image}" "${pinned}" &>/dev/null 2>&1; then
2222
+ docker_exec rmi "${image}" &>/dev/null 2>&1 || true
2223
+ printf '%s' "${pinned}"
2224
+ return 0
2225
+ fi
2226
+
2227
+ printf '%s' "${image}"
2228
+ return 0
2229
+ }
2230
+
1873
2231
  # Install OpenClaw npm package on the host (for process manager / raw_exec modes).
1874
2232
  # Skipped when using official Docker image.
1875
2233
  _install_openclaw_npm() {
@@ -1921,25 +2279,15 @@ install_openclaw() {
1921
2279
  return 1
1922
2280
  fi
1923
2281
 
2282
+ local docker_tag="${OPENCLAW_DOCKER_TAG}"
2283
+ local configured_tag=""
2284
+
1924
2285
  if [[ "$DRY_RUN" == "1" ]]; then
1925
- ui_info "[dry-run] Would: npm install -g --prefix openclaw@${OPENCLAW_NPM_VERSION}"
1926
- ui_info "[dry-run] Would: docker build -t jishushell-openclaw:<version> (npm package + Python)"
2286
+ ui_info "[dry-run] Would: docker pull ${docker_tag} (fallback: local build)"
1927
2287
  return 0
1928
2288
  fi
1929
2289
 
1930
- # ── Step 1: Install OpenClaw npm package (used as docker build context) ──
1931
- _install_openclaw_npm || return 1
1932
-
1933
- # Resolve versioned tag from installed package (e.g. jishushell-openclaw:2026.3.31)
1934
- local pkg_dir="${JISHUSHELL_HOME}/packages/openclaw"
1935
- local oc_ver
1936
- oc_ver="$(node -p "require('${pkg_dir}/lib/node_modules/openclaw/package.json').version" 2>/dev/null || echo "")"
1937
- local docker_tag="${OPENCLAW_DOCKER_TAG}"
1938
- if [[ -n "$oc_ver" ]]; then
1939
- docker_tag="jishushell-openclaw:${oc_ver}"
1940
- fi
1941
-
1942
- # ── Step 2: Ensure Docker daemon is accessible ────────────────────────────
2290
+ # ── Step 1: Ensure Docker daemon is accessible ────────────────────────────
1943
2291
  if ! docker_exec info &>/dev/null 2>&1; then
1944
2292
  if command -v sg &>/dev/null 2>/dev/null && sg docker -c "docker info" &>/dev/null 2>&1; then
1945
2293
  DOCKER_CMD_PREFIX="sg docker -c"
@@ -1950,7 +2298,7 @@ install_openclaw() {
1950
2298
  else
1951
2299
  ui_warn "Docker daemon is not reachable"
1952
2300
  if [[ "$OS" == "macos" ]]; then
1953
- ui_warn "Make sure Docker Desktop is running"
2301
+ ui_warn "Run: COLIMA_HOME=${_COLIMA_HOME} colima start ${_COLIMA_PROFILE}"
1954
2302
  else
1955
2303
  ui_warn "Ensure Docker is running: sudo systemctl start docker"
1956
2304
  fi
@@ -1958,49 +2306,90 @@ install_openclaw() {
1958
2306
  fi
1959
2307
  fi
1960
2308
 
1961
- # ── Step 3: Skip if image already exists ──────────────────────────────────
2309
+ # ── Step 2: Reuse the currently configured pinned image if it already
2310
+ # exists locally. This avoids re-pulling :latest on machines where the
2311
+ # running JishuShell service has already migrated panel.json from a mutable
2312
+ # tag (e.g. :latest) to an immutable version tag (e.g. :2026.4.9).
2313
+ configured_tag="$(_read_openclaw_image_from_panel)"
2314
+ if [[ -n "${configured_tag}" ]] && docker_exec image inspect "${configured_tag}" &>/dev/null 2>&1; then
2315
+ OPENCLAW_IMAGE="$(_pin_openclaw_image_if_needed "${configured_tag}")"
2316
+ _save_openclaw_image_to_panel "${OPENCLAW_IMAGE}"
2317
+ ui_success "Docker image ${OPENCLAW_IMAGE} already exists — reusing configured image"
2318
+ return 0
2319
+ fi
2320
+
2321
+ # ── Step 3: Skip if the requested install tag already exists ─────────────
1962
2322
  if docker_exec image inspect "${docker_tag}" &>/dev/null 2>&1; then
1963
- OPENCLAW_IMAGE="${docker_tag}"
1964
- _save_openclaw_image_to_panel "${docker_tag}"
1965
- ui_success "Docker image ${docker_tag} already exists — skipping"
2323
+ OPENCLAW_IMAGE="$(_pin_openclaw_image_if_needed "${docker_tag}")"
2324
+ _save_openclaw_image_to_panel "${OPENCLAW_IMAGE}"
2325
+ ui_success "Docker image ${OPENCLAW_IMAGE} already exists — skipping"
1966
2326
  return 0
1967
2327
  fi
1968
2328
 
1969
- # ── Step 4: Build Docker image (npm package + Python) ─────────────────────
1970
- local pkg_dir="${JISHUSHELL_HOME}/packages/openclaw"
2329
+ # ── Step 4: Pull from registry, fallback to local build ──────────────────
2330
+ ui_info "Pulling OpenClaw Docker image: ${docker_tag} ..."
2331
+ log_detail ""
2332
+ log_detail "[$(date '+%H:%M:%S')] docker pull ${docker_tag}"
2333
+ if log_cmd docker_exec pull "${docker_tag}"; then
2334
+ OPENCLAW_IMAGE="$(_pin_openclaw_image_if_needed "${docker_tag}")"
2335
+ _save_openclaw_image_to_panel "${OPENCLAW_IMAGE}"
2336
+ ui_success "OpenClaw Docker image pulled: ${OPENCLAW_IMAGE}"
2337
+ return 0
2338
+ fi
2339
+
2340
+ # ── Step 3b: Fallback — build locally using bundled Dockerfile ────
2341
+ ui_warn "Pull failed, falling back to local build..."
2342
+
2343
+ # Locate the bundled Dockerfile.openclaw-slim + openclaw-entry.sh.
2344
+ # Both ship at the npm package root, alongside the install/ directory,
2345
+ # so from this script's perspective they are one level up.
2346
+ local script_dir
2347
+ if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
2348
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2349
+ else
2350
+ script_dir="${PWD}"
2351
+ fi
2352
+ local dockerfile_src="${script_dir}/../Dockerfile.openclaw-slim"
2353
+ local entrypoint_src="${script_dir}/../openclaw-entry.sh"
2354
+
2355
+ if [[ ! -f "${dockerfile_src}" || ! -f "${entrypoint_src}" ]]; then
2356
+ ui_error "Bundled build files not found near ${script_dir}/.."
2357
+ ui_error "Expected: Dockerfile.openclaw-slim and openclaw-entry.sh"
2358
+ return 1
2359
+ fi
2360
+
2361
+ local build_ctx
2362
+ build_ctx="$(mktemp -d)"
2363
+ trap "rm -rf '${build_ctx}'" EXIT
1971
2364
 
1972
- # Write Dockerfile into the npm package directory (build context)
1973
- cat > "${pkg_dir}/Dockerfile" << 'DOCKERFILE'
1974
- FROM node:22-slim
1975
- USER root
1976
- RUN apt-get update && apt-get install -y --no-install-recommends \
1977
- procps hostname curl git lsof openssl \
1978
- python3 python3-pip python3-venv python3-dev && \
1979
- ln -sf /usr/bin/python3 /usr/local/bin/python && \
1980
- rm -rf /var/lib/apt/lists/*
1981
- WORKDIR /app
1982
- COPY --chown=node:node lib/node_modules/ ./node_modules/
1983
- RUN ln -sf /app/node_modules/openclaw/openclaw.mjs /app/openclaw.mjs && \
1984
- ln -sf /app/node_modules/openclaw/openclaw.mjs /usr/local/bin/openclaw && \
1985
- cp /app/node_modules/openclaw/package.json /app/package.json 2>/dev/null || true
1986
- USER node
1987
- CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
1988
- DOCKERFILE
1989
-
1990
- ui_info "Building Docker image: ${docker_tag} (npm package + Python)..."
2365
+ cp "${dockerfile_src}" "${build_ctx}/Dockerfile.openclaw-slim"
2366
+ cp "${entrypoint_src}" "${build_ctx}/openclaw-entry.sh"
2367
+
2368
+ # Query current OpenClaw version from npm so the --build-arg busts the
2369
+ # Docker layer cache for the `RUN npm install openclaw@${ver}` step.
2370
+ # Fall back to "latest" if npm is unreachable.
2371
+ local openclaw_ver
2372
+ openclaw_ver="$(npm view openclaw version 2>/dev/null)"
2373
+ if [[ -z "${openclaw_ver}" ]]; then
2374
+ openclaw_ver="latest"
2375
+ fi
2376
+ log_detail "Resolved OpenClaw version for build: ${openclaw_ver}"
2377
+
2378
+ ui_info "Building OpenClaw Docker image locally: ${docker_tag} (openclaw@${openclaw_ver}) ..."
1991
2379
  log_detail ""
1992
- log_detail "[$(date '+%H:%M:%S')] docker build -t ${docker_tag} ${pkg_dir}"
1993
- if log_cmd docker_exec build --network=host -t "${docker_tag}" "${pkg_dir}"; then
2380
+ log_detail "[$(date '+%H:%M:%S')] docker build --network=host --build-arg OPENCLAW_VERSION=${openclaw_ver} -f Dockerfile.openclaw-slim -t ${docker_tag} ${build_ctx}"
2381
+ if log_cmd docker_exec build --network=host \
2382
+ --build-arg "OPENCLAW_VERSION=${openclaw_ver}" \
2383
+ -f "${build_ctx}/Dockerfile.openclaw-slim" \
2384
+ -t "${docker_tag}" "${build_ctx}"; then
1994
2385
  OPENCLAW_IMAGE="${docker_tag}"
1995
2386
  _save_openclaw_image_to_panel "${docker_tag}"
1996
- local _local_tag="jishushell-openclaw:local"
1997
- if [[ "${docker_tag}" != "${_local_tag}" ]]; then
1998
- docker_exec tag "${docker_tag}" "${_local_tag}" 2>/dev/null || true
1999
- fi
2000
- rm -f "${pkg_dir}/Dockerfile"
2001
- ui_success "OpenClaw Docker image built: ${docker_tag} (with Python)"
2387
+ rm -rf "${build_ctx}"
2388
+ trap - EXIT
2389
+ ui_success "OpenClaw Docker image built: ${docker_tag}"
2002
2390
  else
2003
- rm -f "${pkg_dir}/Dockerfile"
2391
+ rm -rf "${build_ctx}"
2392
+ trap - EXIT
2004
2393
  ui_error "Failed to build OpenClaw Docker image"
2005
2394
  return 1
2006
2395
  fi
@@ -2028,6 +2417,14 @@ _prompt_openclaw_skip() {
2028
2417
  esac
2029
2418
  }
2030
2419
 
2420
+ jishushell_package_spec() {
2421
+ if [[ "${JISHUSHELL_VERSION_OVERRIDE}" == "1" || "${JISHUSHELL_NPM_VERSION}" != "latest" ]]; then
2422
+ printf 'jishushell@%s' "${JISHUSHELL_NPM_VERSION}"
2423
+ return 0
2424
+ fi
2425
+ printf 'jishushell'
2426
+ }
2427
+
2031
2428
  # show_install_plan [--with-jishushell]
2032
2429
  show_install_plan() {
2033
2430
  local with_jishushell=0
@@ -2043,17 +2440,19 @@ show_install_plan() {
2043
2440
  ui_kv "Node.js" "$(if [[ $SKIP_NODE -eq 1 ]]; then echo 'skip'; else echo "v${NODE_VERSION} via nvm v${NVM_VERSION}"; fi)"
2044
2441
  ui_kv "Docker" "$(if [[ $SKIP_DOCKER -eq 1 ]]; then echo 'skip'; else echo 'latest stable'; fi)"
2045
2442
  ui_kv "Nomad" "$(if [[ $SKIP_NOMAD -eq 1 ]]; then echo 'skip'; else echo "v${NOMAD_VERSION}"; fi)"
2046
- ui_kv "OpenClaw" "$(if [[ \"${SKIP_OPENCLAW}\" == \"1\" ]]; then echo 'skip'; else echo \"openclaw@${OPENCLAW_NPM_VERSION} + docker build ${OPENCLAW_DOCKER_TAG}\"; fi)"
2443
+ ui_kv "OpenClaw" "$(if [[ "${SKIP_OPENCLAW}" == "1" ]]; then echo 'skip'; else echo "docker pull ${OPENCLAW_DOCKER_TAG}"; fi)"
2047
2444
  if [[ $with_jishushell -eq 1 ]]; then
2048
2445
  local _plan_jishu
2049
2446
  if [[ $SKIP_JISHUSHELL -eq 1 ]]; then
2050
2447
  _plan_jishu="skip"
2051
2448
  else
2052
2449
  local _plan_tgz=""
2053
- for _c in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
2054
- [[ -f "$_c" ]] && { _plan_tgz="$(basename "$_c")"; break; }
2055
- done
2056
- _plan_jishu="${_plan_tgz:+npm install -g ${_plan_tgz} (local)}${_plan_tgz:-npm install -g jishushell}"
2450
+ if [[ "${JISHUSHELL_VERSION_OVERRIDE}" != "1" && "${JISHUSHELL_NPM_VERSION}" == "latest" ]]; then
2451
+ for _c in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
2452
+ [[ -f "$_c" ]] && { _plan_tgz="$(basename "$_c")"; break; }
2453
+ done
2454
+ fi
2455
+ _plan_jishu="${_plan_tgz:+npm install -g ${_plan_tgz} (local)}${_plan_tgz:-npm install -g $(jishushell_package_spec)}"
2057
2456
  fi
2058
2457
  ui_kv "JishuShell" "$_plan_jishu"
2059
2458
  ui_kv "JishuShell service" "$(if [[ $SKIP_JISHUSHELL_SERVICE -eq 1 ]]; then echo 'skip'; else echo 'register autostart'; fi)"
@@ -2114,7 +2513,15 @@ show_summary() {
2114
2513
  fi
2115
2514
 
2116
2515
  if [[ "${SKIP_OPENCLAW}" != "1" ]]; then
2117
- if [[ -n "${OPENCLAW_IMAGE}" ]] && docker_exec image inspect "${OPENCLAW_IMAGE}" &>/dev/null 2>&1; then
2516
+ local _summary_openclaw_image=""
2517
+ _summary_openclaw_image="$(_read_openclaw_image_from_panel)"
2518
+ if [[ -z "${_summary_openclaw_image}" && -n "${OPENCLAW_IMAGE}" ]]; then
2519
+ _summary_openclaw_image="${OPENCLAW_IMAGE}"
2520
+ fi
2521
+
2522
+ if [[ -n "${_summary_openclaw_image}" ]] && docker_exec image inspect "${_summary_openclaw_image}" &>/dev/null 2>&1; then
2523
+ ui_kv "OpenClaw" "✓ ${_summary_openclaw_image}"
2524
+ elif [[ -n "${OPENCLAW_IMAGE}" ]] && docker_exec image inspect "${OPENCLAW_IMAGE}" &>/dev/null 2>&1; then
2118
2525
  ui_kv "OpenClaw" "✓ ${OPENCLAW_IMAGE}"
2119
2526
  elif [[ "$DRY_RUN" == "1" ]]; then
2120
2527
  ui_kv "OpenClaw" "- dry-run"
@@ -2210,16 +2617,20 @@ install_jishushell() {
2210
2617
  fi
2211
2618
 
2212
2619
  if [[ "$DRY_RUN" == "1" ]]; then
2620
+ local jishushell_pkg_spec
2621
+ jishushell_pkg_spec="$(jishushell_package_spec)"
2213
2622
  local _dry_reg=""
2214
2623
  [[ -n "${NPM_REGISTRY:-}" ]] && _dry_reg=" --registry ${NPM_REGISTRY}"
2215
2624
  local _dry_tgz=""
2216
- for _c in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
2217
- [[ -f "$_c" ]] && { _dry_tgz="$_c"; break; }
2218
- done
2625
+ if [[ "${JISHUSHELL_VERSION_OVERRIDE}" != "1" && "${JISHUSHELL_NPM_VERSION}" == "latest" ]]; then
2626
+ for _c in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
2627
+ [[ -f "$_c" ]] && { _dry_tgz="$_c"; break; }
2628
+ done
2629
+ fi
2219
2630
  if [[ -n "$_dry_tgz" ]]; then
2220
2631
  ui_info "[dry-run] Would: npm install -g ${_dry_tgz} (local package)"
2221
2632
  else
2222
- ui_info "[dry-run] Would: npm install -g jishushell${_dry_reg}"
2633
+ ui_info "[dry-run] Would: npm install -g ${jishushell_pkg_spec}${_dry_reg}"
2223
2634
  fi
2224
2635
  ui_info "[dry-run] Would write wrapper: ${JISHUSHELL_BIN_DIR}/jishushell-panel-start"
2225
2636
  return 0
@@ -2250,6 +2661,8 @@ install_jishushell() {
2250
2661
  return 1
2251
2662
  fi
2252
2663
 
2664
+ local jishushell_pkg_spec
2665
+ jishushell_pkg_spec="$(jishushell_package_spec)"
2253
2666
  local npm_registry_args=()
2254
2667
  if [[ -n "${NPM_REGISTRY:-}" ]]; then
2255
2668
  if [[ ! "$NPM_REGISTRY" =~ ^https?:// ]]; then
@@ -2257,9 +2670,9 @@ install_jishushell() {
2257
2670
  return 1
2258
2671
  fi
2259
2672
  npm_registry_args=("--registry" "${NPM_REGISTRY}")
2260
- ui_info "Installing jishushell from ${NPM_REGISTRY}..."
2673
+ ui_info "Installing ${jishushell_pkg_spec} from ${NPM_REGISTRY}..."
2261
2674
  else
2262
- ui_info "Installing jishushell from public npm registry..."
2675
+ ui_info "Installing ${jishushell_pkg_spec} from public npm registry..."
2263
2676
  fi
2264
2677
 
2265
2678
  # When jishushell is already installed (e.g. running as npm postinstall hook),
@@ -2268,12 +2681,14 @@ install_jishushell() {
2268
2681
  # Prefer a local .tgz package in the same directory as this script.
2269
2682
  local tgz_path=""
2270
2683
  local _tgz_candidate
2271
- for _tgz_candidate in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
2272
- if [[ -f "$_tgz_candidate" ]]; then
2273
- tgz_path="$_tgz_candidate"
2274
- break
2275
- fi
2276
- done
2684
+ if [[ "${JISHUSHELL_VERSION_OVERRIDE}" != "1" && "${JISHUSHELL_NPM_VERSION}" == "latest" ]]; then
2685
+ for _tgz_candidate in "${JISHU_SCRIPT_DIR}"/jishushell-*.tgz; do
2686
+ if [[ -f "$_tgz_candidate" ]]; then
2687
+ tgz_path="$_tgz_candidate"
2688
+ break
2689
+ fi
2690
+ done
2691
+ fi
2277
2692
 
2278
2693
  # Export a sentinel so post-install.sh (triggered by npm's postinstall
2279
2694
  # lifecycle hook) knows it was launched from inside jishu-install.sh and
@@ -2289,10 +2704,10 @@ install_jishushell() {
2289
2704
  return 1
2290
2705
  fi
2291
2706
  else
2292
- log_detail "[$(date '+%H:%M:%S')] ${npm_bin} install -g jishushell ${npm_registry_args[*]:-}"
2293
- if ! log_cmd "$npm_bin" install -g jishushell ${npm_registry_args[@]+"${npm_registry_args[@]}"}; then
2707
+ log_detail "[$(date '+%H:%M:%S')] ${npm_bin} install -g ${jishushell_pkg_spec} ${npm_registry_args[*]:-}"
2708
+ if ! log_cmd "$npm_bin" install -g "${jishushell_pkg_spec}" "${npm_registry_args[@]}"; then
2294
2709
  unset JISHU_RUNNING_IN_INSTALLER
2295
- ui_error "npm install -g jishushell failed"
2710
+ ui_error "npm install -g ${jishushell_pkg_spec} failed"
2296
2711
  return 1
2297
2712
  fi
2298
2713
  fi
@@ -2462,7 +2877,9 @@ WantedBy=multi-user.target"
2462
2877
  ${SUDO} systemctl restart jishushell 2>/dev/null || true
2463
2878
  ui_success "JishuShell systemd service updated and restarted"
2464
2879
  else
2465
- ui_success "JishuShell systemd service already installed"
2880
+ # Package may have been upgraded — always restart to pick up new code
2881
+ ${SUDO} systemctl restart jishushell 2>/dev/null || true
2882
+ ui_success "JishuShell systemd service restarted"
2466
2883
  fi
2467
2884
  }
2468
2885
 
@@ -2594,8 +3011,17 @@ parse_args() {
2594
3011
  shift
2595
3012
  OPENCLAW_NPM_VERSION="${1:?--openclaw-version requires a version argument (e.g. 3.24)}"
2596
3013
  ;;
3014
+ --openclaw-docker-tag)
3015
+ shift
3016
+ OPENCLAW_DOCKER_TAG="${1:?--openclaw-docker-tag requires a tag argument (e.g. ghcr.io/x-aijishu/openclaw-runtime:2026.4.9)}"
3017
+ ;;
2597
3018
  --skip-jishushell) SKIP_JISHUSHELL=1 ;;
2598
3019
  --skip-jishushell-service) SKIP_JISHUSHELL_SERVICE=1 ;;
3020
+ --jishushell-version)
3021
+ shift
3022
+ JISHUSHELL_NPM_VERSION="${1:?--jishushell-version requires a version argument (e.g. 0.4.9)}"
3023
+ JISHUSHELL_VERSION_OVERRIDE=1
3024
+ ;;
2599
3025
  --skip)
2600
3026
  shift
2601
3027
  IFS=',' read -ra _steps <<< "${1:-}"
@@ -2661,10 +3087,16 @@ Options:
2661
3087
  --skip-docker Skip step 2: Docker installation
2662
3088
  --skip-nomad Skip step 3: Nomad installation
2663
3089
  --skip-openclaw Skip step 4: OpenClaw installation
3090
+ --openclaw-docker-tag <tag>
3091
+ Pull a specific OpenClaw image tag
3092
+ (e.g. --openclaw-docker-tag ghcr.io/x-aijishu/openclaw-runtime:2026.4.9)
2664
3093
  --skip-jishushell Skip step 5: JishuShell installation
2665
3094
  --skip-jishushell-service Skip step 6: JishuShell service registration
3095
+ --jishushell-version <ver>
3096
+ Install a specific jishushell version
3097
+ (e.g. --jishushell-version 0.4.9)
2666
3098
  --registry <url> Use a custom npm registry for all installs
2667
- (e.g. --registry http://10.188.0.22:4873/)
3099
+ (e.g. --registry http://127.0.0.1:4873/)
2668
3100
  --yes, -y Skip all confirmation prompts
2669
3101
  --help, -h Show this help message
2670
3102
 
@@ -2672,7 +3104,7 @@ Steps:
2672
3104
  1 Node.js (via nvm)
2673
3105
  2 Docker
2674
3106
  3 Nomad
2675
- 4 OpenClaw (npm install + docker build)
3107
+ 4 OpenClaw (docker pull / local build)
2676
3108
  5 JishuShell
2677
3109
  6 JishuShell service registration (autostart)
2678
3110
 
@@ -2680,12 +3112,16 @@ Environment variables:
2680
3112
  JISHU_NODE_VERSION Specify Node.js major version (default: ${NODE_VERSION})
2681
3113
  JISHU_NVM_VERSION Specify nvm version (default: ${NVM_VERSION})
2682
3114
  JISHU_NOMAD_VERSION Specify Nomad version (default: ${NOMAD_VERSION})
3115
+ JISHUSHELL_NPM_VERSION
3116
+ Specify jishushell npm package version (default: latest)
2683
3117
  OPENCLAW_NPM_VERSION Specify openclaw npm package version (default: latest)
2684
- OPENCLAW_DOCKER_TAG Override built Docker image tag (default: jishushell-base:v1)
3118
+ OPENCLAW_DOCKER_TAG Override OpenClaw Docker image tag (default: ${OPENCLAW_DOCKER_TAG})
2685
3119
  NPM_REGISTRY Custom npm registry URL (same as --registry flag)
2686
3120
 
2687
- OpenClaw version flag (equivalent to OPENCLAW_NPM_VERSION env var):
2688
- --openclaw-version <ver> Install a specific openclaw version, e.g. --openclaw-version 3.24
3121
+ Version flags:
3122
+ --jishushell-version <ver> Install a specific jishushell version, e.g. --jishushell-version 0.4.9
3123
+ --openclaw-version <ver> Install a specific openclaw version, e.g. --openclaw-version 3.24
3124
+ --openclaw-docker-tag <tag> Pull a specific OpenClaw image tag, e.g. --openclaw-docker-tag ghcr.io/x-aijishu/openclaw-runtime:2026.4.9
2689
3125
  NO_PROMPT Set to 1 to skip interactive prompts
2690
3126
  VERBOSE Set to 1 for verbose output
2691
3127
 
@@ -2735,27 +3171,36 @@ _prompt_install_confirm() {
2735
3171
  echo -e " commercial use, competitive offerings, or redistribution."
2736
3172
  echo ""
2737
3173
 
2738
- if [[ $SKIP_DOCKER -eq 0 ]]; then
3174
+ if [[ $SKIP_DOCKER -eq 0 && "$OS" == "linux" ]]; then
2739
3175
  echo -e " ${BOLD}Docker Engine${NC}"
2740
- echo -e " ${MUTED} URL : https://github.com/moby/moby${NC}"
2741
- echo -e " ${MUTED} License : Apache License, Version 2.0${NC}"
2742
- echo -e " ${MUTED} https://www.apache.org/licenses/LICENSE-2.0${NC}"
2743
- echo -e " ${MUTED} Author : Docker, Inc.${NC}"
3176
+ echo -e " ${MUTED} Docker Engine (container runtime — Linux)${NC}"
3177
+ echo -e " ${MUTED} URL : https://github.com/moby/moby${NC}"
3178
+ echo -e " ${MUTED} License : Apache License, Version 2.0${NC}"
3179
+ echo -e " ${MUTED} Author : Docker, Inc.${NC}"
3180
+ echo -e " ${MUTED} https://github.com/moby/moby/blob/master/LICENSE${NC}"
3181
+ echo ""
3182
+ fi
3183
+ if [[ $SKIP_DOCKER -eq 0 && "$OS" == "macos" ]]; then
3184
+ echo -e " ${BOLD}Colima${NC}"
3185
+ echo -e " ${MUTED} Colima (container runtime — macOS)${NC}"
3186
+ echo -e " ${MUTED} URL : https://github.com/abiosoft/colima${NC}"
3187
+ echo -e " ${MUTED} License : MIT License${NC}"
3188
+ echo -e " ${MUTED} https://github.com/abiosoft/colima/blob/main/LICENSE${NC}"
3189
+ echo -e " ${MUTED} Author : Abiola Ibrahim${NC}"
2744
3190
  echo ""
2745
3191
  fi
2746
3192
  if [[ $SKIP_NOMAD -eq 0 ]]; then
2747
- echo -e " ${BOLD}Nomad v${NOMAD_VERSION}+ (>= 1.7.0)${NC}"
2748
- echo -e " ${MUTED} URL : https://github.com/hashicorp/nomad${NC}"
2749
- echo -e " ${MUTED} License : Business Source License 1.1 (BSL 1.1)${NC}"
2750
- echo -e " ${MUTED} https://github.com/hashicorp/nomad/blob/main/LICENSE${NC}"
2751
- echo -e " ${MUTED} Licensor: International Business Machines Corporation (IBM)${NC}"
2752
- echo -e " ${MUTED} Work : Nomad Version 1.7.0 or later. (c) 2024 IBM Corp.${NC}"
3193
+ echo -e " ${BOLD}Nomad${NC}"
3194
+ echo -e " ${MUTED} Nomad v${NOMAD_VERSION} (last MPL 2.0 release in the 1.6.x line)${NC}"
3195
+ echo -e " ${MUTED} URL : https://github.com/hashicorp/nomad/tree/v${NOMAD_VERSION}${NC}"
3196
+ echo -e " ${MUTED} License : Mozilla Public License 2.0${NC}"
3197
+ echo -e " ${MUTED} https://github.com/hashicorp/nomad/blob/v${NOMAD_VERSION}/LICENSE${NC}"
3198
+ echo -e " ${MUTED} Author : HashiCorp, Inc.${NC}"
2753
3199
  echo ""
2754
3200
  fi
2755
- echo -e " ${MUTED}─────────────────────────────────────────────────────────${NC}"
2756
- echo -e " ${MUTED}By continuing you acknowledge that you have read the above${NC}"
2757
- echo -e " ${MUTED}notices and agree to each package's license terms.${NC}"
2758
- echo -e " ${MUTED}sudo privileges are required to write to system directories.${NC}"
3201
+ echo -e " ${ACCENT}─────────────────────────────────────────────────────────${NC}"
3202
+ echo -e " By continuing you acknowledge that you have read the above"
3203
+ echo -e " notices and agree to each package's license terms."
2759
3204
  echo ""
2760
3205
  } >/dev/tty
2761
3206
 
@@ -2767,6 +3212,9 @@ _prompt_install_confirm() {
2767
3212
  exit 0
2768
3213
  ;;
2769
3214
  esac
3215
+
3216
+ echo "" >/dev/tty
3217
+ echo -e " ${INFO}sudo privileges are required to write to system directories.${NC}" >/dev/tty
2770
3218
  echo "" >/dev/tty
2771
3219
  }
2772
3220
 
@@ -2885,7 +3333,7 @@ _jishu_install_main() {
2885
3333
  # so the keepalive's early sudo -n true calls are intentional no-ops.
2886
3334
  if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then
2887
3335
  if [[ -z "${_SUDO_KEEPALIVE_PID:-}" ]]; then
2888
- ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &
3336
+ ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &>/dev/null &
2889
3337
  _SUDO_KEEPALIVE_PID=$!
2890
3338
  disown "$_SUDO_KEEPALIVE_PID" 2>/dev/null || true
2891
3339
  fi
@@ -2900,10 +3348,6 @@ _jishu_install_main() {
2900
3348
  detect_os
2901
3349
  detect_arch
2902
3350
  show_install_plan --with-jishushell
2903
- if [[ "$OS" == "macos" ]]; then
2904
- ui_warn "macOS is not supported yet — coming soon!"
2905
- exit 0
2906
- fi
2907
3351
  _prompt_install_confirm
2908
3352
  check_sudo
2909
3353
  ensure_prerequisites