Files
claude-plugin-nixbuild/bin/nbshell
T
Oleks cbcf122422 Initial commit: nixbuild.net operator plugin
Haiku ops agent + 3 tools (nbshell expect+PTY admin-shell driver, nbapi
read-only HTTP client, nb-substituters guarded cache manager) + 2 skills
(nixbuild-settings, nixbuild-usage). Encodes the two-control-surface model
and the path-style-substituter-URL gotcha.
2026-06-01 11:17:40 +03:00

113 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# nbshell — drive nixbuild.net's interactive admin shell non-interactively.
#
# nixbuild.net's account administration (settings, ssh-keys, tokens, billing)
# lives ONLY behind `ssh <host> shell`, and that shell is stubborn:
# - `ssh -T <host> shell` with piped stdin produces NO output (needs a PTY).
# - `ssh -tt <host> shell` is rejected: "pty not supported".
# - `ssh <host> shell <cmd>` (command as ssh args) hits the *build-runner*
# channel, which needs the `run:write` permission and fails with
# "Authorization failed".
# The only thing that works is a real local PTY wrapping `ssh -T ... shell`,
# fed one command at a time and synchronised on the `nixbuild.net> ` prompt.
# `expect` provides exactly that. (The HTTP API at api.nixbuild.net is
# read-only — no settings endpoints — so it cannot substitute for this.)
#
# Usage:
# nbshell 'settings substituters --show'
# nbshell 'settings substituters --add https://cache.example.org' \
# 'settings substituters --show'
# printf '%s\n' 'settings usage' | nbshell - # commands on stdin
#
# Each argument (or each stdin line) is one shell command line, run in order
# in a single session. Output is cleaned: banner and `nixbuild.net> ` prompts
# stripped, CRs removed. Exit code is expect's (0 unless the session failed).
#
# Env:
# NIXBUILD_SSH_HOST override host (default: eu.nixbuild.net / userConfig)
# NBSHELL_RAW=1 do not strip banner/prompts (debug)
# NBSHELL_TIMEOUT per-prompt timeout seconds (default 30)
#
# Note: `exit` is NOT a valid shell command — the session ends on EOF, which
# this script handles via expect `close`.
set -euo pipefail
HOST="${NIXBUILD_SSH_HOST:-eu.nixbuild.net}"
TIMEOUT="${NBSHELL_TIMEOUT:-30}"
# Collect commands from argv, or from stdin if the sole arg is "-".
cmds=()
if [ "$#" -eq 1 ] && [ "$1" = "-" ]; then
while IFS= read -r line; do
[ -n "$line" ] && cmds+=("$line")
done
else
cmds=("$@")
fi
if [ "${#cmds[@]}" -eq 0 ]; then
echo "nbshell: no commands given" >&2
echo "usage: nbshell 'settings substituters --show' [more commands...]" >&2
exit 2
fi
# Locate expect: prefer a system binary, else run it ephemerally from nixpkgs
# (no profile/-env install — matches the declarative-only house rule).
if command -v expect >/dev/null 2>&1; then
EXPECT=(expect)
elif command -v nix >/dev/null 2>&1; then
EXPECT=(nix run nixpkgs#expect --)
else
echo "nbshell: need either 'expect' on PATH or 'nix' to fetch it" >&2
exit 3
fi
script="$(mktemp "${TMPDIR:-/tmp}/nbshell.XXXXXX.exp")"
trap 'rm -f "$script"' EXIT
{
echo "set timeout ${TIMEOUT}"
echo 'log_user 1'
echo 'proc wp {} {'
echo ' expect {'
echo ' -re {nixbuild\.net>\s*$} { return }'
echo ' timeout { puts stderr "nbshell: TIMEOUT waiting for prompt"; return }'
echo ' }'
echo '}'
# -T: do NOT request a remote PTY (nixbuild rejects that); expect itself
# provides the local PTY the shell needs to render. Disable mux so we get
# a clean dedicated channel.
echo "spawn ssh -T -o ControlMaster=no -o ControlPath=none ${HOST} shell"
echo 'wp'
for c in "${cmds[@]}"; do
# Escape backslashes and double-quotes for the Tcl string literal.
esc=${c//\\/\\\\}
esc=${esc//\"/\\\"}
echo "send \"${esc}\r\""
echo 'wp'
done
# The remote shell has no `exit` command and won't EOF on its own; close the
# PTY ourselves. `close` then a bare `expect eof` races and throws
# "spawn id ... not open" — wrap both so the session always ends cleanly
# with exit status 0.
echo 'catch { close }'
echo 'catch { expect eof }'
echo 'exit 0'
} >"$script"
run_expect() { "${EXPECT[@]}" "$script" 2>&1; }
if [ "${NBSHELL_RAW:-0}" = "1" ]; then
run_expect
exit "${PIPESTATUS[0]}"
fi
# Clean output: drop the static welcome banner, SSH PQ/MOTD warnings, the
# command listing, the spawn echo, any Tcl close/eof traceback, and the
# `nixbuild.net> ` prompt prefix; normalise CRs and squeeze blank lines.
run_expect | tr -d '\r' | grep -vE \
'WARNING|store now, decrypt|may need to be upgraded|openssh\.com/pq|^\*\*|^•|nixbuild\.net •|^Welcome to nixbuild|^This shell allows|^and retrieve information|^Account id:|^Storage used:|^You have no free|^Free build time left:|^Available shell commands:|^ (help|settings|usage|ssh-keys|billing|tokens|builds) |^Run .help COMMAND|^For help on subcommands|^For more documentation|^https://docs\.nixbuild\.net/configuration|^spawn ssh |^spawn id |expect: spawn id|^ *while executing|^ *invoked from within|^"(expect eof|close)"|^ *\(file ' \
| sed -E 's/^nixbuild\.net> ?//' \
| cat -s