diff --git a/CHANGELOG.md b/CHANGELOG.md index ca514501e9d..17cd474b3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io. - Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg. - Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker. - Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai. diff --git a/scripts/install.sh b/scripts/install.sh index 20e6840ee91..eb7df218f22 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -404,7 +404,7 @@ is_shell_function() { is_gum_raw_mode_failure() { local err_log="$1" [[ -s "$err_log" ]] || return 1 - grep -Eiq 'setrawmode' "$err_log" + grep -Eiq 'setrawmode|inappropriate ioctl' "$err_log" } run_with_spinner() { @@ -412,13 +412,25 @@ run_with_spinner() { shift if [[ -n "$GUM" ]] && gum_is_tty && ! is_shell_function "${1:-}"; then - local gum_err + local gum_err gum_out gum_err="$(mktempfile)" - if "$GUM" spin --spinner dot --title "$title" -- "$@" 2>"$gum_err"; then + gum_out="$(mktempfile)" + if "$GUM" spin --spinner dot --title "$title" -- "$@" >"$gum_out" 2>"$gum_err"; then + if is_gum_raw_mode_failure "$gum_out" || is_gum_raw_mode_failure "$gum_err"; then + GUM="" + GUM_STATUS="skipped" + GUM_REASON="gum raw mode unavailable" + ui_warn "Spinner unavailable in this terminal; continuing without spinner" + "$@" + return $? + fi + if [[ -s "$gum_out" ]]; then + cat "$gum_out" + fi return 0 fi local gum_status=$? - if is_gum_raw_mode_failure "$gum_err"; then + if is_gum_raw_mode_failure "$gum_err" || is_gum_raw_mode_failure "$gum_out"; then GUM="" GUM_STATUS="skipped" GUM_REASON="gum raw mode unavailable" diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index fe7efa4167f..3d568f21fef 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -1,5 +1,7 @@ import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; const SCRIPT_PATH = "scripts/install.sh"; @@ -123,4 +125,52 @@ describe("install.sh macOS Homebrew Node behavior", () => { expect(result.stdout).not.toContain("Node.js v24 was installed"); expect(result.stdout).not.toContain("Add this to your shell profile"); }); + + it("falls back when gum reports raw-mode ioctl failures", () => { + expect(script).toContain("setrawmode|inappropriate ioctl"); + expect(script).toContain( + 'if "$GUM" spin --spinner dot --title "$title" -- "$@" >"$gum_out" 2>"$gum_err"; then', + ); + expect(script).toContain( + 'if is_gum_raw_mode_failure "$gum_out" || is_gum_raw_mode_failure "$gum_err"; then', + ); + expect(script).toContain( + 'ui_warn "Spinner unavailable in this terminal; continuing without spinner"', + ); + expect(script).toContain('"$@"\n return $?'); + }); + + it("reruns spinner-wrapped commands when gum reports ioctl failure", () => { + const dir = mkdtempSync(join(tmpdir(), "openclaw-install-sh-gum-")); + try { + const gumPath = join(dir, "gum"); + const commandPath = join(dir, "command"); + const markerPath = join(dir, "marker"); + writeFileSync( + gumPath, + "#!/usr/bin/env bash\nprintf 'inappropriate ioctl for device\\n'\nexit 0\n", + { mode: 0o755 }, + ); + writeFileSync(commandPath, `#!/usr/bin/env bash\nprintf 'ran' >"${markerPath}"\n`, { + mode: 0o755, + }); + + const result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + gum_is_tty() { return 0; } + GUM="${gumPath}" + run_with_spinner "Installing node" "${commandPath}" + cat "${markerPath}" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + "Spinner unavailable in this terminal; continuing without spinner", + ); + expect(result.stdout).toContain("ran"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); });