fix(installer): handle macos gum node failures

This commit is contained in:
Peter Steinberger
2026-04-26 06:35:56 +01:00
parent be8a3617d9
commit cbf9c60f1d
3 changed files with 68 additions and 5 deletions

View File

@@ -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.

View File

@@ -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"

View File

@@ -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 });
}
});
});