mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
* fix(install): describe automatic setup and guard truncated downloads
The help text said the installer only installs the CLI and that
`rea setup` must be run separately, but the installer starts
`rea setup` interactively whenever a terminal is available. Describe
that behaviour and the --no-setup/--no-prompt opt-out.
Wrap the installer body in `main` and call it on the last line, so a
truncated `curl | bash` download fails to parse instead of executing a
partial script. Behaviour is otherwise unchanged.
Add an installer scenario that runs a copy truncated right after the
npm install line and requires that it prints nothing and never invokes
npm; it fails against the previous script.
Validation: bash -n, shellcheck 0.10.0, installScript boundary tests.
* fix(install): require the closing token before running main
Wrapping the installer in main() kept a truncated download from running
a partial body, but the last line was a bare `main "$@"`. If the piped
download stopped right after `main` (or `main `), bash saw a complete
command at EOF and ran main with no positional parameters. Under the
documented `curl ... | bash -s -- --dry-run` (or --version / --no-setup)
path that silently dropped the caller's flags, so a dry run could resolve
the latest release, run npm, and start setup.
The call now sits in a brace group:
{
main "$@"
exit $?
}
Bash does not run the group until it has read the closing brace, so
every truncation inside it is a syntax error and nothing executes; a
truncation before the group leaves only function definitions. A comment
in install.sh records why the bare call is not enough.
Validation: a new boundary test pipes install.sh to `bash -s --
--dry-run` truncated at every byte offset from the end of main() to EOF
and asserts that npm and the rea binary are never invoked; offsets
inside the group must exit non-zero with a bash parse error and no
stdout, offsets before it must print nothing, and the complete group
must finish the dry run. Reproduced the original bug by cutting the old
script after `main`: the dry run invoked npm install. `bash -n` and
shellcheck 0.11.0 are clean.
* fix(install): wrap the whole script in the truncation guard
The previous guard only wrapped the final `main "$@"` invocation, so the
preamble streamed on the `curl ... | bash` path before any incomplete
compound command was open. A download cut off right after the bytes
`set` reached EOF with a complete, argument-less `set`, which prints every
shell variable, so inherited tokens could reach stdout.
The brace group now opens on line 2, directly after the shebang, and
closes on the last line, enclosing `set -euo pipefail`, the constants, the
functions and the final `main "$@"` / `exit $?`. Bash must therefore read
the whole file before running anything; any earlier cut is a syntax error
at EOF. `set -euo pipefail` still applies inside the group (checked
manually for `false`, an unbound variable, and a failing function body),
and shellcheck 0.11.0 and `bash -n` are clean.
The truncation test now pipes every prefix of install.sh, from byte 0 to
EOF (about 6,100 cuts, run 16 at a time with a minimal environment that
includes a sentinel variable), into `bash -s -- --dry-run`. Before the
group opens it must exit quietly; inside the group it must fail with a
syntax error; in every case stdout stays empty and neither npm nor rea is
invoked. Only the complete script performs the dry run.
141 lines
6.0 KiB
Bash
Executable File
141 lines
6.0 KiB
Bash
Executable File
#!/bin/bash
|
|
{
|
|
# Truncation guard: this brace group opens on line 2 and closes on the last
|
|
# line, so bash must read the entire file before it runs any command. On the
|
|
# `curl ... | bash` path, a download cut off anywhere before the final `}` is
|
|
# an unterminated compound command, which bash reports as a syntax error at
|
|
# EOF without running anything. Without the guard, a cut right after `set`
|
|
# would run the bare builtin and print every shell variable, and a cut right
|
|
# after `main` would run the installer without the caller's flags.
|
|
set -euo pipefail
|
|
|
|
PACKAGE="rea-agents"
|
|
REPOSITORY="morluto/rea"
|
|
numeric_identifier='(0|[1-9][0-9]*)'
|
|
prerelease_identifier="(${numeric_identifier}|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
|
|
build_identifier='[0-9A-Za-z-]+'
|
|
version_pattern="${numeric_identifier}\\.${numeric_identifier}\\.${numeric_identifier}(-${prerelease_identifier}(\\.${prerelease_identifier})*)?(\\+${build_identifier}(\\.${build_identifier})*)?"
|
|
version="${REA_VERSION:-}"
|
|
dry_run=false
|
|
start_setup=true
|
|
verbose=false
|
|
|
|
fail() {
|
|
printf 'REA installation failed: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
|
|
|
|
Installs the REA CLI, then starts `rea setup` interactively to configure Hopper
|
|
and agents when a terminal is available. Pass --no-setup (or --no-prompt) to
|
|
skip setup and run `rea setup` later.
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
while (($# > 0)); do
|
|
case "$1" in
|
|
--version)
|
|
(($# >= 2)) || fail "--version requires a value."
|
|
version="$2"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
dry_run=true
|
|
shift
|
|
;;
|
|
--no-setup|--no-prompt)
|
|
start_setup=false
|
|
shift
|
|
;;
|
|
--verbose)
|
|
verbose=true
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*) fail "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$verbose" == true ]]; then
|
|
set -x
|
|
fi
|
|
|
|
platform="$(uname -s)"
|
|
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
|
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
|
|
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
|
|
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
|
|
|
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
|
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
|
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
|
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
|
|
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
|
|
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
|
|
fi
|
|
|
|
if [[ -n "$version" ]]; then
|
|
[[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version."
|
|
else
|
|
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
|
|
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
|
|
[[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
|
|
version="${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
# npm's exact-version resolver cleans build metadata from the requested version,
|
|
# so it may install the base release and make the post-install identity check fail.
|
|
if [[ "$version" =~ \+ ]]; then
|
|
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
|
|
fi
|
|
|
|
install_args=(install --global)
|
|
if [[ "$platform" == "Linux" ]]; then
|
|
install_args+=(--prefix "$HOME/.local")
|
|
install_bin="$HOME/.local/bin/rea"
|
|
else
|
|
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
|
|
install_bin="$npm_prefix/bin/rea"
|
|
fi
|
|
install_args+=("$PACKAGE@$version")
|
|
|
|
printf 'REA install plan\n'
|
|
printf ' Version: %s\n' "$version"
|
|
printf ' Runtime: Node.js %s\n' "$node_version"
|
|
printf ' Command:'
|
|
printf ' %q' npm "${install_args[@]}"
|
|
printf '\n'
|
|
printf ' Binary: %s\n' "$install_bin"
|
|
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
|
|
|
|
if [[ "$dry_run" == true ]]; then
|
|
printf 'Dry run complete; no changes made.\n'
|
|
exit 0
|
|
fi
|
|
|
|
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
|
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
|
|
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
|
|
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
|
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
|
|
|
|
printf 'REA %s is installed.\n' "$version"
|
|
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
|
|
printf 'Starting REA setup...\n'
|
|
"$install_bin" setup </dev/tty >/dev/tty
|
|
else
|
|
printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
|
|
fi
|
|
}
|
|
|
|
main "$@"
|
|
exit $?
|
|
}
|