Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
14 kB · 411 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412#!/usr/bin/env bash# ac-desktop — Single command for the Aesthetic Computer Electron app.## Usage:# ac-desktop dev [--prod] [--watch] [--shell] Run locally with hot tools# ac-desktop test Quick local launch (no build)# ac-desktop build [--mac|--win|--linux|--all] Build distributable artifacts# ac-desktop install [--force] [--version=X.Y.Z] Download + install latest published DMG# ac-desktop reinstall Build local + replace /Applications copy# ac-desktop release [patch|minor|major|X.Y.Z] Bump version + tag (no push)# ac-desktop publish Build, sign, notarize, upload to releases.aesthetic.computer# ac-desktop status Show local / installed / published versions side-by-side# ac-desktop monitor [--interval=SECS] Poll the release feed and announce new versions# ac-desktop logs Tail the running app's stderr# ac-desktop open Launch the installed app# ac-desktop kill Quit running instances# ac-desktop icons Regenerate tray + app icons# ac-desktop autodeploy [--interval=SECS] Retry push + publish + install until network cooperates# ac-desktop doctor Check signing creds, electron, dist, installed app## Subcommands delegate to existing scripts (dev.fish, install.fish, release.fish,# build-all-platforms.fish, scripts/build-publish-host-mac.sh) so behavior stays# in one place. Run `ac-desktop` with no args (or `--help`) to see this list.set -e# Resolve symlinks so SCRIPT_DIR points at the real ac-electron/ even when# this script is invoked through ~/.local/bin/ac-desktop or similar._self="$0"while [ -L "${_self}" ]; do _link="$(readlink "${_self}")" case "${_link}" in /*) _self="${_link}" ;; *) _self="$(cd "$(dirname "${_self}")" && pwd)/${_link}" ;; esacdoneSCRIPT_DIR="$(cd "$(dirname "${_self}")" && pwd)"APP_NAME="Aesthetic.Computer.app"INSTALLED_APP="/Applications/${APP_NAME}"RELEASES_URL="https://releases.aesthetic.computer/desktop"# Colorsif [ -t 1 ]; then C="\033[0;36m"; G="\033[0;32m"; Y="\033[0;33m"; R="\033[0;31m"; M="\033[0;35m"; D="\033[2m"; N="\033[0m"else C=""; G=""; Y=""; R=""; M=""; D=""; N=""filog() { printf "${C}[ac-desktop]${N} %s\n" "$*"; }ok() { printf "${G}✓${N} %s\n" "$*"; }warn() { printf "${Y}…${N} %s\n" "$*"; }err() { printf "${R}✗${N} %s\n" "$*" >&2; }hdr() { printf "\n${M}━━━ %s ━━━${N}\n" "$*"; }CMD="${1:-help}"shift || true# ------------------------------------------------------------ helpersusage() { sed -n 's/^# \{0,1\}//;3,/^$/p' "$0" | head -40}local_version() { node -p "require('${SCRIPT_DIR}/package.json').version" 2>/dev/null || echo "?"}installed_version() { if [ -f "${INSTALLED_APP}/Contents/Info.plist" ]; then defaults read "${INSTALLED_APP}/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "?" else echo "(not installed)" fi}published_version() { curl -fsSL "${RELEASES_URL}/latest-mac.yml" 2>/dev/null \ | awk '/^version:/ {print $2; exit}' \ | tr -d '\r"'}require_mac() { if [ "$(uname)" != "Darwin" ]; then err "This subcommand requires macOS (host is $(uname))" exit 1 fi}cd_repo() { cd "${SCRIPT_DIR}"; }# ------------------------------------------------------------ subcommandscmd_help() { usage; }cmd_dev() { cd_repo log "Delegating to dev.fish $*" exec fish ./dev.fish "$@"}cmd_test() { cd_repo require_mac log "Quick local launch (electron .)" exec npm start}cmd_build() { cd_repo local target="--mac" for arg in "$@"; do case "$arg" in --mac|--win|--linux|--all) target="$arg" ;; esac done case "$target" in --mac) require_mac log "Building macOS DMG (universal)…" npm run build:mac ;; --win) log "Building Windows installer…" npm run build:win ;; --linux) log "Building Linux packages…" npm run build:linux ;; --all) log "Cross-platform build (delegating to build-all-platforms.fish)…" exec fish ./build-all-platforms.fish ;; esac ok "Build complete — see ${SCRIPT_DIR}/dist/" ls -1 dist/ 2>/dev/null | head -10 || true}cmd_install() { require_mac cd_repo log "Delegating to install.fish $*" exec fish ./install.fish "$@"}cmd_reinstall() { require_mac cd_repo hdr "REINSTALL — build local + replace ${INSTALLED_APP}" log "Quitting any running instances…" osascript -e 'tell application "Aesthetic.Computer" to quit' >/dev/null 2>&1 || true pkill -f "Aesthetic.Computer" 2>/dev/null || true sleep 0.5 # Local reinstall path: build the .app but skip notarization (which needs # Apple's notary service over the network) — we strip quarantine ourselves # below so Gatekeeper still opens it cleanly. Use `ac-desktop publish` for # the full signed+notarized release. log "Building macOS app (notarization skipped for local install)…" npx electron-builder --mac --universal -c.mac.notarize=false # Locate the built .app — universal target writes to dist/mac-universal/. local built_app for candidate in "dist/mac-universal/${APP_NAME}" "dist/mac/${APP_NAME}" "dist/mac-arm64/${APP_NAME}" "dist/mac-x64/${APP_NAME}"; do if [ -d "${SCRIPT_DIR}/${candidate}" ]; then built_app="${SCRIPT_DIR}/${candidate}" break fi done if [ -z "${built_app}" ]; then err "Could not find the built .app under dist/ — looked in mac-universal, mac, mac-arm64, mac-x64" ls -1 dist/ 2>/dev/null exit 1 fi log "Built bundle: ${built_app}" if [ -d "${INSTALLED_APP}" ]; then log "Removing old installation…" rm -rf "${INSTALLED_APP}" fi log "Copying to /Applications…" cp -R "${built_app}" "${INSTALLED_APP}" xattr -dr com.apple.quarantine "${INSTALLED_APP}" 2>/dev/null || true ok "Reinstalled $(installed_version) → ${INSTALLED_APP}" log "Launching…" open "${INSTALLED_APP}"}cmd_release() { cd_repo log "Delegating to release.fish $*" exec fish ./release.fish "$@"}cmd_publish() { require_mac cd_repo hdr "PUBLISH — sign + notarize + upload to releases.aesthetic.computer" log "Local version: $(local_version)" log "Currently published: $(published_version || echo "?")" log "Running npm run publish:mac (electron-builder --publish always)…" npm run publish:mac ok "Publish kicked off — verify with: ac-desktop status"}cmd_status() { local lv iv pv lv=$(local_version) iv=$(installed_version) pv=$(published_version) [ -z "${pv}" ] && pv="(unreachable)" hdr "STATUS" printf " ${D}local source ${N}: %s\n" "${lv}" printf " ${D}installed app${N}: %s\n" "${iv}" printf " ${D}published ${N}: %s\n" "${pv}" echo "" if [ "${lv}" = "${iv}" ] && [ "${lv}" = "${pv}" ]; then ok "All three versions match" else if [ "${lv}" != "${pv}" ]; then warn "local source (${lv}) differs from published (${pv}) — run 'ac-desktop publish' to ship" fi if [ "${iv}" != "${pv}" ] && [ "${iv}" != "(not installed)" ]; then warn "installed (${iv}) differs from published (${pv}) — run 'ac-desktop install' to update" fi if [ "${iv}" = "(not installed)" ]; then warn "no app in /Applications — run 'ac-desktop install' (published) or 'ac-desktop reinstall' (local build)" fi fi}cmd_monitor() { local interval=30 for arg in "$@"; do case "$arg" in --interval=*) interval="${arg#--interval=}" ;; esac done log "Polling ${RELEASES_URL}/latest-mac.yml every ${interval}s. Ctrl-C to stop." local last="" while true; do local cur cur=$(published_version || echo "") if [ -n "${cur}" ] && [ "${cur}" != "${last}" ]; then if [ -z "${last}" ]; then log "Current published version: ${G}${cur}${N}" else ok "New published version: ${last} → ${G}${cur}${N} 🎉" fi last="${cur}" fi sleep "${interval}" done}cmd_logs() { require_mac log "Tailing macOS unified log for Aesthetic.Computer (Ctrl-C to stop)" exec log stream --predicate 'process == "Aesthetic.Computer" OR process == "Aesthetic.Computer Helper"' --style compact}cmd_open() { require_mac if [ ! -d "${INSTALLED_APP}" ]; then err "Not installed. Try: ac-desktop install" exit 1 fi log "Launching ${INSTALLED_APP}" open "${INSTALLED_APP}"}cmd_kill() { require_mac log "Quitting running instances…" osascript -e 'tell application "Aesthetic.Computer" to quit' >/dev/null 2>&1 || true pkill -f "Aesthetic.Computer" 2>/dev/null || true sleep 0.3 if pgrep -f "Aesthetic.Computer" >/dev/null 2>&1; then warn "Still running — forcing" pkill -9 -f "Aesthetic.Computer" 2>/dev/null || true fi ok "All instances stopped"}cmd_icons() { cd_repo log "Regenerating icons…" npm run icons ok "Icons rebuilt under build/icons/"}cmd_autodeploy() { require_mac cd_repo local interval=120 for arg in "$@"; do case "$arg" in --interval=*) interval="${arg#--interval=}" ;; esac done hdr "AUTODEPLOY — retry push + publish + install until reachable" log "Probe interval: ${interval}s. Ctrl-C to stop." log "Will: 1) git push origin main 2) ac-desktop publish 3) ac-desktop install" echo "" # Step 1: push to knot. We don't merge/force — just retry until knot's # SSH is reachable. (knot.aesthetic.computer:22 is what airline wifi # typically blocks.) while true; do log "[1/3] Probing knot SSH…" if timeout 15 ssh -o BatchMode=yes -o ConnectTimeout=10 \ git@knot.aesthetic.computer 2>&1 \ | grep -qE "(does not provide shell access|Welcome|gitea|git)"; then ok "knot reachable" break fi # Some hosts hang up cleanly with no banner — accept that too if SSH exited 0. timeout 15 ssh -o BatchMode=yes -o ConnectTimeout=10 \ git@knot.aesthetic.computer true >/dev/null 2>&1 && { ok "knot reachable (silent)"; break; } warn "knot not reachable yet — sleeping ${interval}s" sleep "${interval}" done log "Pushing to origin/main…" if ! git push origin main; then err "push failed — bailing. Resolve manually then rerun: ac-desktop autodeploy" exit 1 fi ok "pushed" # Step 2: publish (signs + notarizes + uploads). Will fail without # internet to Apple TSA / notary service / releases bucket — retry the # whole thing if so. while true; do log "[2/3] Running publish…" if PYTHON=/opt/homebrew/bin/python3.12 npm_config_python=/opt/homebrew/bin/python3.12 \ npm run publish:mac; then ok "published" break fi warn "publish failed (likely network) — sleeping ${interval}s and retrying" sleep "${interval}" done # Step 3: install the now-published version. log "[3/3] Installing published version locally…" fish ./install.fish --force || warn "install step failed — try manually: ac-desktop install --force" ok "autodeploy complete" cmd_status}cmd_doctor() { cd_repo hdr "DOCTOR" # Node + npm if command -v node >/dev/null; then ok "node $(node --version)"; else err "node missing"; fi if command -v npm >/dev/null; then ok "npm $(npm --version)"; else err "npm missing"; fi if command -v fish >/dev/null; then ok "fish $(fish --version | head -1)"; else warn "fish missing — dev.fish/install.fish/release.fish won't run"; fi # Local Electron install if [ -f "${SCRIPT_DIR}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron" ] \ || [ -f "${SCRIPT_DIR}/node_modules/electron/dist/electron" ]; then ok "electron installed in node_modules" else warn "electron not in node_modules — run: cd ${SCRIPT_DIR} && npm install" fi # Vault (signing creds) local vault="${SCRIPT_DIR}/../aesthetic-computer-vault/ac-electron" if [ -d "${vault}" ]; then ok "vault present at ${vault}" [ -f "${vault}/.env" ] && ok "vault .env present" || warn "vault .env missing — signed builds may fail" else warn "vault dir missing (${vault}) — signed/published builds need it" fi # Versions cmd_status}# ------------------------------------------------------------ dispatchcase "${CMD}" in help|-h|--help|"") cmd_help ;; dev) cmd_dev "$@" ;; test|run) cmd_test "$@" ;; build) cmd_build "$@" ;; install) cmd_install "$@" ;; reinstall|rebuild) cmd_reinstall "$@" ;; release|bump) cmd_release "$@" ;; publish|ship) cmd_publish "$@" ;; status|st) cmd_status "$@" ;; monitor|watch) cmd_monitor "$@" ;; logs|log) cmd_logs "$@" ;; open|launch) cmd_open "$@" ;; kill|stop|quit) cmd_kill "$@" ;; icons) cmd_icons "$@" ;; autodeploy|adeploy) cmd_autodeploy "$@" ;; doctor|check) cmd_doctor "$@" ;; *) err "Unknown subcommand: ${CMD}" echo "" cmd_help exit 1 ;;esac