#!/usr/bin/env bash # StarCommand installer — one bundle: local sidecar + galaxy board. # # curl -fsSL https://sc.ideative.tools/install.sh | bash # tar xzf starcommand.tar.gz && ./starcommand/install.sh # # Env (all optional): # STARCOMMAND_SITE where to fetch the tarball (default https://sc.ideative.tools) # STARCOMMAND_TARBALL skip the download; install this .tar.gz # STARCOMMAND_HOME install prefix (default ~/.starcommand) # STARCOMMAND_BIN directory for the `starcommand` symlink (default ~/.local/bin) # STARCOMMAND_PORT bind port (default: first free in 4177–4190) # STARCOMMAND_NO_LAUNCHD set to skip the login-item daemon # STARCOMMAND_NO_OPEN set to skip opening the board in a browser # STARCOMMAND_SKIP_NPM set to skip `npm ci` (tests) # STARCOMMAND_SKIP_START set to skip launching the daemon (tests) set -euo pipefail SITE="${STARCOMMAND_SITE:-https://sc.ideative.tools}" PREFIX="${STARCOMMAND_HOME:-${HOME}/.starcommand}" BIN_DIR="${STARCOMMAND_BIN:-${HOME}/.local/bin}" DEFAULT_PORT=4177 PORT_SCAN_LAST=4190 die() { echo "error: $*" >&2; exit 1; } info() { echo "→ $*"; } # Directory of this script when it lives inside a packed bundle (install.sh next to bin/). # A piped `curl | bash` has no real script path — do not treat the current checkout as the bundle. bundle_dir() { local src here src="${BASH_SOURCE[0]:-$0}" [[ "$src" == /* ]] || src="$PWD/$src" [[ "$(basename "$src")" == install.sh ]] || return 1 here="$(cd "$(dirname "$src")" && pwd)" || return 1 [[ -f "$here/bin/starcommand.js" && -f "$here/package.json" ]] || return 1 printf '%s\n' "$here" } port_in_use() { bash -c "echo >/dev/tcp/127.0.0.1/$1" >/dev/null 2>&1 } # Prefer 4177; if this Mac already has a board (a git checkout, another prefix), keep going. pick_port() { local p if [[ -n "${STARCOMMAND_PORT:-}" ]]; then printf '%s\n' "$STARCOMMAND_PORT" return fi for ((p = DEFAULT_PORT; p <= PORT_SCAN_LAST; p++)); do port_in_use "$p" && continue printf '%s\n' "$p" return done return 1 } # Tarball may be flat (bin/…) or wrapped (starcommand/bin/…). unwrap() { local dir="$1" if [[ -f "$dir/bin/starcommand.js" ]]; then printf '%s\n' "$dir" elif [[ -f "$dir/starcommand/bin/starcommand.js" ]]; then printf '%s\n' "$dir/starcommand" else return 1 fi } if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then die "do not run this as root — the sidecar holds tracker tokens and lives in your home directory" fi [[ -n "${HOME:-}" ]] || die "HOME is not set" uname_s="$(uname -s)" case "$uname_s" in Darwin) ;; Linux) echo "note: StarCommand is developed on macOS; Linux is experimental (no launchd)." ;; *) die "unsupported OS ($uname_s) — need macOS (Linux may work from a checkout)" ;; esac command -v node >/dev/null || die "Node.js 20+ is required — https://nodejs.org" command -v npm >/dev/null || die "npm is required (it ships with Node.js)" command -v tar >/dev/null || die "tar is required" node_major="$(node -p 'process.versions.node.split(".")[0]')" [[ "$node_major" -ge 20 ]] || die "Node.js 20+ is required (found $(node -v))" TMP="$(mktemp -d "${TMPDIR:-/tmp}/starcommand-install.XXXXXX")" cleanup() { rm -rf "$TMP"; } trap cleanup EXIT NEW="" IN_PLACE=0 BUNDLE="$(bundle_dir || true)" if [[ -n "${STARCOMMAND_TARBALL:-}" ]]; then tarball="$STARCOMMAND_TARBALL" [[ -f "$tarball" ]] || die "STARCOMMAND_TARBALL is not a file: $tarball" tarball="$(cd "$(dirname "$tarball")" && pwd)/$(basename "$tarball")" mkdir -p "$TMP/extract" tar -xzf "$tarball" -C "$TMP/extract" NEW="$(unwrap "$TMP/extract")" || die "the archive is not a StarCommand bundle (missing bin/starcommand.js)" elif [[ -n "$BUNDLE" ]]; then info "installing the bundle at $BUNDLE" if [[ "$BUNDLE" == "$PREFIX" ]]; then NEW="$PREFIX" IN_PLACE=1 else mkdir -p "$TMP/app" cp -R "$BUNDLE"/. "$TMP/app" NEW="$TMP/app" fi else command -v curl >/dev/null || die "curl is required" info "downloading StarCommand from $SITE" curl -fsSL "$SITE/starcommand.tar.gz" -o "$TMP/starcommand.tar.gz" curl -fsSL "$SITE/starcommand.tar.gz.sha256" -o "$TMP/starcommand.tar.gz.sha256" ( cd "$TMP" if command -v shasum >/dev/null; then shasum -a 256 -c starcommand.tar.gz.sha256 else sha256sum -c starcommand.tar.gz.sha256 fi ) mkdir -p "$TMP/extract" tar -xzf "$TMP/starcommand.tar.gz" -C "$TMP/extract" NEW="$(unwrap "$TMP/extract")" || die "the archive is not a StarCommand bundle (missing bin/starcommand.js)" fi [[ -f "$NEW/bin/starcommand.js" && -f "$NEW/package.json" ]] || die "the archive is not a StarCommand bundle (missing bin/starcommand.js)" if [[ "$IN_PLACE" -eq 0 && -d "$PREFIX" ]]; then info "upgrading existing install at $PREFIX (keeping .env, connections.yaml, data/)" [[ -f "$PREFIX/.env" ]] && cp "$PREFIX/.env" "$NEW/.env" [[ -f "$PREFIX/connections.yaml" ]] && cp "$PREFIX/connections.yaml" "$NEW/connections.yaml" if [[ -d "$PREFIX/data" ]]; then rm -rf "$NEW/data" cp -R "$PREFIX/data" "$NEW/data" fi fi chmod 700 "$NEW" || true mkdir -p "$(dirname "$PREFIX")" if [[ -z "${STARCOMMAND_SKIP_NPM:-}" ]]; then info "installing dependencies in $NEW" (cd "$NEW" && npm ci --omit=dev) info "building the board UI" (cd "$NEW" && node scripts/build-web.mjs) fi if [[ "$IN_PLACE" -eq 0 ]]; then OLD="$PREFIX.old" rm -rf "$OLD" if [[ -d "$PREFIX" ]]; then mv "$PREFIX" "$OLD"; fi mv "$NEW" "$PREFIX" rm -rf "$OLD" fi chmod 700 "$PREFIX" || true trap - EXIT rm -rf "$TMP" mkdir -p "$BIN_DIR" ln -sfn "$PREFIX/bin/starcommand.js" "$BIN_DIR/starcommand" chmod +x "$PREFIX/bin/starcommand.js" info "installed to $PREFIX" if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then echo "note: add $BIN_DIR to your PATH to run \`starcommand\` from any directory" fi if [[ -z "${STARCOMMAND_SKIP_START:-}" ]]; then PORT="$(pick_port)" || die "ports ${DEFAULT_PORT}–${PORT_SCAN_LAST} are all in use — stop one or set STARCOMMAND_PORT" if [[ "$PORT" != "$DEFAULT_PORT" ]]; then info "port ${DEFAULT_PORT} is in use — starting on $PORT" fi if [[ "$uname_s" == Darwin && -z "${STARCOMMAND_NO_LAUNCHD:-}" && "$PORT" == "$DEFAULT_PORT" && -f "$PREFIX/scripts/install-daemon.sh" ]]; then info "installing the login-item daemon (launchd)" bash "$PREFIX/scripts/install-daemon.sh" web || echo "note: launchd install skipped — \`starcommand start\` still works" fi info "starting StarCommand on port $PORT" "$PREFIX/bin/starcommand.js" start --port "$PORT" url="" for _ in 1 2 3 4 5 6 7 8; do url="$("$PREFIX/bin/starcommand.js" url --port "$PORT" 2>/dev/null || true)" [[ "$url" == http* ]] && break sleep 1 done echo echo "StarCommand board: ${url:-http://127.0.0.1:${PORT}/}" echo "Setup opens on first visit. Claude or Cursor CLI on this Mac is the dispatch rail." if [[ "$uname_s" == Darwin && -z "${STARCOMMAND_NO_OPEN:-}" && "$url" == http* ]] && command -v open >/dev/null; then open "$url" || true fi fi