#!/bin/bash # # Twork installer. # # curl -fsSL https://epiknowledge.com/install.sh | bash # # Downloading with curl matters: macOS only attaches com.apple.quarantine to # files written by browsers and mail clients, so an app installed this way is # not held back by Gatekeeper the way a double-clicked download would be. # # © 2026 Epiknowledge. All Rights Reserved. set -euo pipefail LATEST_URL="${TWORK_LATEST_URL:-https://epiknowledge.com/latest.json}" INSTALL_DIR="${TWORK_INSTALL_DIR:-/Applications}" TARBALL_URL="${TWORK_TARBALL_URL:-}" LAUNCH=1 UNINSTALL=0 while [[ $# -gt 0 ]]; do case "$1" in --dir) INSTALL_DIR="$2"; shift 2 ;; --url) TARBALL_URL="$2"; shift 2 ;; --latest-url) LATEST_URL="$2"; shift 2 ;; --no-open) LAUNCH=0; shift ;; --uninstall) UNINSTALL=1; shift ;; -h|--help) cat <<'USAGE' Twork installer --dir install location (default /Applications) --url install a specific tarball instead of the latest release --latest-url where to look up the current version --no-open do not launch Twork when finished --uninstall remove Twork.app and print how to delete local data USAGE exit 0 ;; *) echo "unknown option: $1" >&2; exit 2 ;; esac done if [[ -t 1 ]]; then B=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[32m'; YEL=$'\033[33m'; R=$'\033[0m' else B=""; DIM=""; RED=""; GREEN=""; YEL=""; R="" fi say() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$B" "$R" "$*"; } warn() { printf '%s warning:%s %s\n' "$YEL" "$R" "$*" >&2; } die() { printf '%s error:%s %s\n' "$RED" "$R" "$*" >&2; exit 1; } APP_NAME="Twork.app" APP_PATH="$INSTALL_DIR/$APP_NAME" quit_running() { if pgrep -x TworkWindow >/dev/null 2>&1 || pgrep -f "$APP_NAME/Contents/MacOS" >/dev/null 2>&1; then step "Quitting the running copy of Twork" osascript -e 'quit app "Twork"' >/dev/null 2>&1 || true for _ in 1 2 3 4 5 6 7 8 9 10; do pgrep -x TworkWindow >/dev/null 2>&1 || break sleep 0.5 done pkill -x TworkWindow >/dev/null 2>&1 || true sleep 1 fi } if [[ "$UNINSTALL" == "1" ]]; then quit_running if [[ -d "$APP_PATH" ]]; then if [[ -w "$INSTALL_DIR" ]]; then rm -rf "$APP_PATH"; else sudo rm -rf "$APP_PATH"; fi say "${GREEN}Removed${R} $APP_PATH" else say "Nothing to remove at $APP_PATH" fi say "" say "Your projects were never touched. Twork's own data still lives in ${B}~/.twork${R}" say "(settings, logs, chat history, downloaded models). Delete it with:" say " ${DIM}rm -rf ~/.twork${R}" exit 0 fi # ---------- environment ---------- [[ "$(uname -s)" == "Darwin" ]] || die "Twork is macOS only (this looks like $(uname -s))." ARCH="$(uname -m)" if [[ "$ARCH" != "arm64" ]]; then die "Twork 1.0 requires Apple Silicon (M1 or newer); this Mac reports $ARCH. Local inference runs on MLX, which is Apple Silicon exclusive." fi MACOS_VER="$(sw_vers -productVersion)" if [[ "${MACOS_VER%%.*}" -lt 13 ]]; then die "Twork needs macOS 13 or newer (this Mac runs $MACOS_VER)." fi command -v curl >/dev/null 2>&1 || die "curl is required but was not found." # ---------- resolve the release ---------- TMP="$(mktemp -d -t twork-install)" cleanup() { rm -rf "$TMP"; } trap cleanup EXIT json_get() { # $1 = file, $2 = key path (plutil syntax, e.g. arm64.url) plutil -extract "$2" raw -o - -- "$1" 2>/dev/null || true } VERSION="" SHA256="" if [[ -z "$TARBALL_URL" ]]; then step "Looking up the latest release" curl -fsSL --connect-timeout 15 --max-time 60 "$LATEST_URL" -o "$TMP/latest.json" \ || die "Could not reach $LATEST_URL — check your network and try again." VERSION="$(json_get "$TMP/latest.json" version)" TARBALL_URL="$(json_get "$TMP/latest.json" arm64.url)" SHA256="$(json_get "$TMP/latest.json" arm64.sha256)" [[ -n "$TARBALL_URL" ]] || die "$LATEST_URL did not contain a download URL for arm64." say " Twork ${B}${VERSION:-unknown}${R}" fi # ---------- download ---------- TARBALL="$TMP/twork.tar.gz" step "Downloading" say " ${DIM}${TARBALL_URL}${R}" curl -fL --connect-timeout 15 --retry 2 --progress-bar "$TARBALL_URL" -o "$TARBALL" \ || die "Download failed." if [[ -n "$SHA256" ]]; then step "Verifying checksum" GOT="$(shasum -a 256 "$TARBALL" | awk '{print $1}')" if [[ "$GOT" != "$SHA256" ]]; then die "Checksum mismatch — the download may be corrupt or tampered with. expected $SHA256 got $GOT" fi else warn "No checksum published for this build; skipping verification." fi # ---------- unpack ---------- step "Unpacking" mkdir -p "$TMP/stage" tar -xzf "$TARBALL" -C "$TMP/stage" || die "Could not unpack the archive." NEW_APP="$TMP/stage/$APP_NAME" [[ -d "$NEW_APP" ]] || NEW_APP="$(find "$TMP/stage" -maxdepth 2 -name "$APP_NAME" -type d | head -1)" [[ -n "$NEW_APP" && -d "$NEW_APP" ]] || die "The archive did not contain $APP_NAME." [[ -x "$NEW_APP/Contents/MacOS/TworkWindow" ]] || die "The archive is missing the Twork executable." # curl does not set it, but a proxy or mirror might. xattr -dr com.apple.quarantine "$NEW_APP" 2>/dev/null || true # ---------- install ---------- SUDO="" if [[ ! -d "$INSTALL_DIR" ]]; then mkdir -p "$INSTALL_DIR" 2>/dev/null || die "$INSTALL_DIR does not exist and could not be created." fi if [[ ! -w "$INSTALL_DIR" ]]; then if [[ -t 0 || -e /dev/tty ]]; then warn "$INSTALL_DIR needs administrator access; you may be asked for your password." SUDO="sudo" else die "$INSTALL_DIR is not writable. Re-run with --dir \"\$HOME/Applications\"." fi fi quit_running step "Installing to $APP_PATH" if [[ -d "$APP_PATH" ]]; then BACKUP="$INSTALL_DIR/.Twork.previous.$$" $SUDO mv "$APP_PATH" "$BACKUP" || die "Could not replace the existing installation." trap '$SUDO rm -rf "$BACKUP" 2>/dev/null || true; cleanup' EXIT fi if ! $SUDO ditto "$NEW_APP" "$APP_PATH"; then if [[ -n "${BACKUP:-}" && -d "$BACKUP" ]]; then $SUDO mv "$BACKUP" "$APP_PATH" || true die "Install failed; your previous version was restored." fi die "Install failed." fi $SUDO xattr -dr com.apple.quarantine "$APP_PATH" 2>/dev/null || true # ---------- done ---------- say "" say "${GREEN}Twork ${VERSION:-} is installed.${R}" say "" say " Open it from Launchpad, Spotlight, or:" say " ${DIM}open -a Twork${R}" say "" say " Everything runs on your machine at 127.0.0.1. Your code is never uploaded." say " Settings and history live in ~/.twork" say " Questions or bugs: support@epiknowledge.com" say "" if [[ "$LAUNCH" == "1" ]]; then open -a "$APP_PATH" 2>/dev/null || true fi