#!/bin/bash # Goodmake for Mac, in one line: # # curl -fsSL https://goodmake.app/install | bash # # What it does, and nothing else: # 1. Downloads Node, only for Goodmake, into ~/Library/Application Support/Goodmake/runtime # (checked against nodejs.org's published checksums). Your own Node, if any, isn't touched. # 2. Installs Goodmake 0.2.0 next to it. # 3. Starts it in the background and at every login (a LaunchAgent in ~/Library/LaunchAgents), # so Goodmake is always at http://localhost:4319 and ⌘⌃⇧4 works without opening anything. # 4. Opens it in your browser. # Your screenshots and groups live in ~/Goodmake. Run this again to update. # Coming from Snapboard (the old name)? This moves the app over; your work moves on its first start. # Uninstall: curl -fsSL https://goodmake.app/uninstall | bash set -euo pipefail # GOODMAKE_, or the old SNAPBOARD_ opt() { local new="GOODMAKE_$1" old="SNAPBOARD_$1"; printf '%s' "${!new:-${!old:-}}"; } SITE="$(opt SITE)"; SITE="${SITE:-https://goodmake.app}" TARBALL="goodmake-0.2.0.tgz" VERSION="0.2.0" NODE_LINE="v22" # Node LTS line for the private runtime # Overridable for testing beside a real install APP_DIR="$(opt APP_DIR)"; APP_DIR="${APP_DIR:-$HOME/Library/Application Support/Goodmake}" LOG_DIR="$(opt LOG_DIR)"; LOG_DIR="${LOG_DIR:-$HOME/Library/Logs/Goodmake}" AGENTS_DIR="$(opt AGENTS_DIR)"; AGENTS_DIR="${AGENTS_DIR:-$HOME/Library/LaunchAgents}" LABEL="$(opt LABEL)"; LABEL="${LABEL:-com.goodmake.app}" PORT="$(opt PORT)"; PORT="${PORT:-4319}" # Where it lived before the rename. Only looked at for a real install (no test overrides). LEGACY="" if [ -z "$(opt APP_DIR)$(opt LABEL)" ]; then LEGACY=1 OLD_APP_DIR="$HOME/Library/Application Support/Snapboard" OLD_LOG_DIR="$HOME/Library/Logs/Snapboard" OLD_LABEL="com.snapboard.app" fi RUNTIME="$APP_DIR/runtime" NODE="$RUNTIME/bin/node" ENTRY="$APP_DIR/app/node_modules/goodmake/bin/goodmake.js" PLIST="$AGENTS_DIR/$LABEL.plist" URL="http://localhost:$PORT" DOMAIN="gui/$(id -u)" say() { printf ' %s\n' "$*"; } fail() { printf '\n ✕ %s\n\n' "$*" >&2; exit 1; } # Escapes a path for the plist (sed, since bash 3.2 and 5 disagree on & in substitutions) xml() { printf '%s' "$1" | sed -e 's/&/\&/g' -e 's//\>/g'; } printf '\n Goodmake %s\n\n' "$VERSION" [ "$(uname -s)" = Darwin ] || fail "This installs the Mac app. On any computer, Goodmake also runs in your browser: $SITE/app/" case "$SITE" in http://* | https://*) ;; *) fail "This installer doesn't know where to download Goodmake from. Copy the command from the Goodmake site." ;; esac # Apple silicon reports x86_64 when the shell runs under Rosetta; Node should be native ARCH=x64 if [ "$(uname -m)" = arm64 ] || [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then ARCH=arm64; fi # 0. Coming from Snapboard: stop it and move its folders over (keeps the Node already downloaded) if [ -n "$LEGACY" ]; then if [ -f "$AGENTS_DIR/$OLD_LABEL.plist" ] || launchctl print "$DOMAIN/$OLD_LABEL" >/dev/null 2>&1; then say "Moving Snapboard over to Goodmake…" launchctl bootout "$DOMAIN/$OLD_LABEL" 2>/dev/null || true rm -f "$AGENTS_DIR/$OLD_LABEL.plist" fi if [ -d "$OLD_APP_DIR" ] && [ ! -e "$APP_DIR" ]; then mv "$OLD_APP_DIR" "$APP_DIR" # The app itself is reinstalled under its new name below rm -rf "$APP_DIR/app" fi if [ -d "$OLD_LOG_DIR" ] && [ ! -e "$LOG_DIR" ]; then mv "$OLD_LOG_DIR" "$LOG_DIR" if [ -f "$LOG_DIR/snapboard.log" ]; then mv "$LOG_DIR/snapboard.log" "$LOG_DIR/goodmake.log"; fi fi rm -rf "$OLD_APP_DIR" "$OLD_LOG_DIR" fi # 1. Node, just for Goodmake (kept across updates; replaced if it's older than 20) if [ -x "$NODE" ] && [ "$("$NODE" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" -ge 20 ]; then say "Node $("$NODE" -v) is ready" else say "Downloading Node for Goodmake (about 45 MB)…" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT BASE="https://nodejs.org/dist/latest-$NODE_LINE.x" LINE="$(curl -fsSL "$BASE/SHASUMS256.txt" | grep -E " node-v[0-9.]+-darwin-$ARCH\.tar\.gz$" | head -n 1)" || true [ -n "$LINE" ] || fail "Couldn't reach nodejs.org. Check your connection and try again." FILE="${LINE##* }" curl -fsSL "$BASE/$FILE" -o "$TMP/$FILE" || fail "Couldn't download Node. Check your connection and try again." (cd "$TMP" && printf '%s\n' "$LINE" | shasum -a 256 -c - >/dev/null 2>&1) || fail "The Node download didn't match its checksum. Try again." rm -rf "$RUNTIME" mkdir -p "$RUNTIME" tar -xzf "$TMP/$FILE" -C "$RUNTIME" --strip-components 1 say "Node $("$NODE" -v) installed" fi # 2. Stop a running copy first, so the update can replace it launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true for _ in 1 2 3 4 5 6 7 8 9 10; do curl -fsS -m 1 "$URL/api/health" >/dev/null 2>&1 || break sleep 0.5 done if curl -fsS -m 1 "$URL/api/health" >/dev/null 2>&1; then fail "Goodmake is already running in a Terminal window (from the earlier setup). Press Ctrl+C in that window, then run this again." fi say "Installing Goodmake $VERSION…" mkdir -p "$APP_DIR/app" # npm's own scripts start with #!/usr/bin/env node, so the private Node goes first on PATH if ! PATH="$RUNTIME/bin:$PATH" npm install --prefix "$APP_DIR/app" --omit=dev --no-fund --no-audit --loglevel=error "$SITE/$TARBALL" >"$APP_DIR/install.log" 2>&1; then fail "The install didn't finish. Details: $APP_DIR/install.log" fi [ -f "$ENTRY" ] || fail "The install didn't finish. Details: $APP_DIR/install.log" # 3. Run in the background now and at every login; restarted if it ever stops unexpectedly mkdir -p "$AGENTS_DIR" "$LOG_DIR" ENV="GOODMAKE_PORT$PORT" for name in HOME WATCH FAKE_SYSTEM; do value="$(opt "$name")" if [ -n "$value" ]; then ENV="$ENVGOODMAKE_$name$(xml "$value")"; fi done cat >"$PLIST" < Label$LABEL ProgramArguments $(xml "$NODE")$(xml "$ENTRY")--service EnvironmentVariables$ENV RunAtLoad KeepAliveSuccessfulExit ThrottleInterval10 ProcessTypeInteractive StandardOutPath$(xml "$LOG_DIR/goodmake.log") StandardErrorPath$(xml "$LOG_DIR/goodmake.log") PLIST launchctl bootstrap "$DOMAIN" "$PLIST" || fail "macOS didn't start Goodmake. Details: $LOG_DIR/goodmake.log" say "Starting…" for _ in $(seq 1 60); do curl -fsS -m 1 "$URL/api/health" >/dev/null 2>&1 && break sleep 0.5 done curl -fsS -m 1 "$URL/api/health" >/dev/null 2>&1 || fail "Goodmake didn't start. Details: $LOG_DIR/goodmake.log" # 4. Open it [ -n "$(opt NO_OPEN)" ] || open "$URL" cat </dev/null; then cat <