diff --git a/setup/channels/signal.ts b/setup/channels/signal.ts index 8462a568e..4e98ee179 100644 --- a/setup/channels/signal.ts +++ b/setup/channels/signal.ts @@ -134,42 +134,74 @@ export async function runSignalChannel(displayName: string): Promise { async function ensureSignalCli(): Promise { const cli = process.env.SIGNAL_CLI_PATH || 'signal-cli'; - const probe = spawnSync(cli, ['--version'], { - stdio: ['ignore', 'pipe', 'pipe'], - }); - if (!probe.error && probe.status === 0) return; + const probeFor = (): boolean => { + const r = spawnSync(cli, ['--version'], { + stdio: ['ignore', 'pipe', 'pipe'], + }); + return !r.error && r.status === 0; + }; + if (probeFor()) return; + note( + [ + "NanoClaw talks to Signal through signal-cli, which isn't installed yet.", + "We'll install it for you now — about 30 seconds, one-time only.", + '', + process.platform === 'darwin' + ? "On this Mac we'll use Homebrew (no admin password needed)." + : "On Linux we'll grab the native release binary (no Java needed) and install it to ~/.local/bin.", + ].join('\n'), + 'Setting up signal-cli', + ); + + const install = await runQuietChild( + 'install-signal-cli', + 'bash', + ['setup/install-signal-cli.sh'], + { + running: 'Installing signal-cli…', + done: 'signal-cli installed.', + }, + ); + + if (install.ok && probeFor()) return; + + const reason = install.terminal?.fields.ERROR; if (process.platform === 'darwin') { note( [ - "NanoClaw talks to Signal through signal-cli, which isn't installed yet.", + "We couldn't install signal-cli automatically.", + reason === 'homebrew_not_installed' + ? ' Reason: Homebrew is not installed.' + : ` Reason: ${reason ?? 'unknown'}.`, '', - 'The quickest way on macOS is Homebrew:', + 'You can install it manually:', '', k.cyan(' brew install signal-cli'), '', - "Install it in another terminal, then re-run setup.", + 'Then re-run setup.', ].join('\n'), - 'signal-cli not found', + "Couldn't install signal-cli", ); } else { note( [ - "NanoClaw talks to Signal through signal-cli, which isn't installed yet.", + "We couldn't install signal-cli automatically.", + ` Reason: ${reason ?? 'unknown'}.`, '', - 'Grab the latest release from GitHub:', + 'You can install it manually from GitHub:', '', k.cyan(' https://github.com/AsamK/signal-cli/releases'), '', - "Install it, make sure `signal-cli --version` works, then re-run setup.", + 'Then re-run setup.', ].join('\n'), - 'signal-cli not found', + "Couldn't install signal-cli", ); } await fail( - 'signal-install', - 'signal-cli is required but not installed.', - 'Install it and re-run setup.', + 'install-signal-cli', + 'signal-cli is required but the auto-install failed.', + 'Install it manually and re-run setup.', ); } diff --git a/setup/install-signal-cli.sh b/setup/install-signal-cli.sh new file mode 100755 index 000000000..870220e89 --- /dev/null +++ b/setup/install-signal-cli.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# install-signal-cli.sh — auto-install signal-cli on the host. +# +# NanoClaw needs `signal-cli` on PATH to talk to Signal. Picks the right +# install method per platform: +# macOS → `brew install signal-cli` (bottled, no Java needed) +# Linux → download latest native binary from GitHub releases to +# ~/.local/bin/signal-cli (no Java, no sudo) +# +# Emits the standard NanoClaw STATUS block on success or failure so the +# `runQuietChild` driver can parse the outcome. + +set -euo pipefail + +VERSION="0.14.3" +INSTALL_DIR="${HOME}/.local/bin" + +emit_status() { + local status=$1 error=${2:-} + echo "=== NANOCLAW SETUP: INSTALL_SIGNAL_CLI ===" + echo "STATUS: ${status}" + [ -n "$error" ] && echo "ERROR: ${error}" + echo "=== END ===" +} + +log() { echo "[install-signal-cli] $*" >&2; } + +uname_s=$(uname) + +if [[ "${uname_s}" == "Darwin" ]]; then + if ! command -v brew >/dev/null 2>&1; then + emit_status failed "homebrew_not_installed" + exit 1 + fi + log "Installing signal-cli via Homebrew…" + brew install signal-cli >&2 || { + emit_status failed "brew_install_failed" + exit 1 + } + emit_status success + exit 0 +fi + +if [[ "${uname_s}" != "Linux" ]]; then + emit_status failed "unsupported_platform_${uname_s}" + exit 1 +fi + +# Linux native build (no Java required) → ~/.local/bin/signal-cli. +URL="https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-native.tar.gz" +TARBALL=$(mktemp -t signal-cli.XXXXXX.tar.gz) + +log "Downloading signal-cli v${VERSION} (~96MB)…" +if ! curl -fLsS -o "${TARBALL}" "${URL}"; then + rm -f "${TARBALL}" + emit_status failed "download_failed" + exit 1 +fi + +log "Extracting…" +EXTRACT_DIR=$(mktemp -d) +if ! tar -xzf "${TARBALL}" -C "${EXTRACT_DIR}"; then + rm -rf "${TARBALL}" "${EXTRACT_DIR}" + emit_status failed "extract_failed" + exit 1 +fi + +mkdir -p "${INSTALL_DIR}" +log "Installing to ${INSTALL_DIR}/signal-cli…" +if ! mv "${EXTRACT_DIR}/signal-cli" "${INSTALL_DIR}/signal-cli"; then + rm -rf "${TARBALL}" "${EXTRACT_DIR}" + emit_status failed "install_failed" + exit 1 +fi +chmod +x "${INSTALL_DIR}/signal-cli" +rm -rf "${TARBALL}" "${EXTRACT_DIR}" + +emit_status success