#!/bin/sh
# Nobie desktop app installer.
#
#   curl -fsSL https://nobie.com/install.sh | sh
#   curl -fsSL https://nobie.com/install.sh | sh -s -- --accept-license   # non-interactive
#
# macOS: downloads the signed Nobie.app DMG from the desktop update feed, has you accept the
#        license, mounts it, copies Nobie.app into /Applications, and installs a /usr/local/bin/nobie
#        wrapper that execs the CLI bundled inside the app.
# Linux: downloads the latest `nobie` binary for this CPU architecture from the release feed, has you
#        accept the license, and installs it to /usr/local/bin/nobie.
#
# Because this script is meant to run through `curl | sh`, its stdin is the download pipe, not your
# keyboard. It therefore reads the license prompt and the sudo password from /dev/tty. If no terminal
# is available, pass --accept-license (or -y) to accept the license non-interactively.
#
# The license text below is the plaintext installer rendering of the agreement owned by
# legal/DesktopAppLicense.rtf. Packaging tests keep this curl-delivered copy aligned with the RTF
# and the public marketing rendering.

set -eu

# --- Configuration ----------------------------------------------------------

FEED_BASE_MACOS="https://updates.prod.nobie.com"
FEED_BASE_LINUX="https://updates.prod.nobie.com/linux"
TERMS_URL="https://nobie.com/desktop-app-license-terms"

APP_PATH="/Applications/Nobie.app"
APP_CLI_RELPATH="Contents/MacOS/nobie"
WRAPPER_PATH="/usr/local/bin/nobie"
LINUX_BIN_PATH="/usr/local/bin/nobie"
DMG_APP_NAME="Nobie.app"

CHANNEL="stable"
ACCEPT_LICENSE="no"
PIN_VERSION=""
MOUNT_POINT=""
WORKDIR=""

# --- Logging ----------------------------------------------------------------

if [ -t 1 ]; then
  C_BOLD="$(printf '\033[1m')"; C_DIM="$(printf '\033[2m')"
  C_RED="$(printf '\033[31m')"; C_GREEN="$(printf '\033[32m')"
  C_RESET="$(printf '\033[0m')"
else
  C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_RESET=""
fi

info()  { printf '%s==>%s %s\n' "$C_BOLD" "$C_RESET" "$*"; }
step()  { printf '%s  ->%s %s\n' "$C_DIM" "$C_RESET" "$*"; }
ok()    { printf '%s==>%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
err()   { printf '%serror:%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }

# --- Cleanup ----------------------------------------------------------------

cleanup() {
  if [ -n "$MOUNT_POINT" ]; then
    hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
    MOUNT_POINT=""
  fi
  if [ -n "$WORKDIR" ] && [ -d "$WORKDIR" ]; then
    rm -rf "$WORKDIR"
    WORKDIR=""
  fi
}
trap cleanup EXIT INT TERM

# --- Usage / argument parsing -----------------------------------------------

usage() {
  cat <<EOF
Nobie installer

Usage: install.sh [options]

Options:
  --accept-license, -y   Accept the Nobie Desktop App License Terms non-interactively.
  --channel <name>       Release channel: stable (default), beta, or internal.
  --version <version>    Install a specific version (macOS only).
  --help, -h             Show this help.
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --accept-license|-y) ACCEPT_LICENSE="yes" ;;
    --channel) shift; [ $# -gt 0 ] || err "--channel requires a value"; CHANNEL="$1" ;;
    --channel=*) CHANNEL="${1#--channel=}" ;;
    --version) shift; [ $# -gt 0 ] || err "--version requires a value"; PIN_VERSION="$1" ;;
    --version=*) PIN_VERSION="${1#--version=}" ;;
    --help|-h) usage; exit 0 ;;
    *) err "unknown option: $1 (try --help)" ;;
  esac
  shift
done

case "$CHANNEL" in
  stable|beta|internal) ;;
  *) err "unknown channel: $CHANNEL (expected stable, beta, or internal)" ;;
esac

# --- Small helpers ----------------------------------------------------------

have() { command -v "$1" >/dev/null 2>&1; }

# http_get URL -> stdout. Fails the script on a transport error.
http_get() {
  if have curl; then
    curl -fsSL "$1"
  elif have wget; then
    wget -qO- "$1"
  else
    err "need curl or wget to download $1"
  fi
}

# download URL DEST. Fails the script on a transport error.
download() {
  if have curl; then
    curl -fsSL -o "$2" "$1"
  elif have wget; then
    wget -qO "$2" "$1"
  else
    err "need curl or wget to download $1"
  fi
}

# sudo_for DIR -> echoes "sudo" when DIR is not writable by us, else nothing.
sudo_for() {
  if [ -w "$1" ]; then printf ''; else
    have sudo || err "$1 is not writable and sudo is not available"
    printf 'sudo'
  fi
}

# Tell the user why elevation is needed, before any sudo call prompts for a password.
# $1 = the action root will perform (e.g. "install nobie into /usr/local/bin").
explain_sudo() {
  info "Administrator access is required to $1."
  step "sudo will prompt for your password (you may not be asked if it is cached)."
}

sha256_of() {
  if have shasum; then shasum -a 256 "$1" | awk '{print $1}';
  elif have sha256sum; then sha256sum "$1" | awk '{print $1}';
  else err "need shasum or sha256sum to verify the download"; fi
}

# --- License gate -----------------------------------------------------------

print_license() {
  cat <<'LICENSE'
================================================================================
Nobie License Agreement

Version 1.0 — July 6, 2026 · © 2026 Nobie, Inc.

Nobie is free for all internal use

Use Nobie for anything inside your company: spreadsheets, analysis, internal tools and automations, even training AI models or agents for your own internal use. Any company size. No fees, no seat counts, no time limits.

This license covers the Nobie desktop app and CLI itself. Optional paid features and services have their own terms.

If you’re a big company and you make money using Nobie in your product or training run, then we make some money

Both apply only after your company passes US $100 million in revenue over the trailing twelve months, counting your whole corporate family together: parents, subsidiaries, and sister companies. Under $100M, everything below is free too.

1. Your product runs on Nobie — $0.50 per end user per month.

If a product or service you offer to external users depends on Nobie to work - it embeds Nobie or calls into it - you pay $0.50 per monthly active end user. When your product stops using Nobie, payment stops.

2. You train AI models on Nobie for distribution — $0.50 per instance-hour.

If you run Nobie as a training environment for an AI model or agent that you sell, license, or let others use, or you sell the training data itself (trajectories, traces, reward signals), you pay $0.50 for each hour each running copy of Nobie is used in training. Billed per minute, and only while the instance is running. Fees are due at train time.

Reporting. Usage is self-reported quarterly to founders@nobie.com. Once a year we may ask for a signed confirmation that your numbers are right. Email us at founders@nobie.com to discuss pricing and support if you have questions.

The rules

•	Don’t resell or redistribute Nobie itself as a standalone product.
•	Don’t reverse engineer Nobie, except where the law says we can’t stop you.
•	Keep our copyright notices intact.

Our promises

•	Your workbooks, data, and documents are yours.
•	Each version of Nobie keeps the license it shipped with. We can’t change the deal on software you already have.

Legal basics

Nobie is licensed, not sold; Nobie, Inc. (a Delaware corporation) keeps all rights in the software. If you send us feedback, we may use it freely. NOBIE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW, WE ARE NOT LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR LOST PROFITS, DATA, OR GOODWILL, AND OUR TOTAL LIABILITY WILL NOT EXCEED THE GREATER OF WHAT YOU PAID US IN THE PRIOR TWELVE MONTHS OR US $100. This agreement ends if you materially breach it and don’t fix it within 30 days of notice; if it ends, stop using Nobie. It is governed by the laws of the State of California, with exclusive venue in San Francisco County, California. You may not assign it without our consent; we may assign it freely. If part of it is unenforceable, the rest stands. This is the entire agreement about the Nobie desktop app and CLI.
================================================================================
LICENSE
}

require_license() {
  if [ "$ACCEPT_LICENSE" = "yes" ]; then
    step "License accepted via --accept-license."
    return 0
  fi
  print_license
  printf '\nFull terms: %s\n\n' "$TERMS_URL"
  # stdin is the curl pipe, so read the prompt/answer from the controlling terminal. /dev/tty can
  # exist as a device node yet fail to open (ENXIO) with no controlling terminal, so treat any
  # failure to write the prompt or read the answer as "no terminal" and fail closed.
  printf 'Do you accept the Nobie Desktop App License Terms? [y/N] ' >/dev/tty 2>/dev/null \
    || err "no terminal available to accept the license; re-run with --accept-license"
  reply=""
  read -r reply </dev/tty 2>/dev/null \
    || err "no terminal available to accept the license; re-run with --accept-license"
  case "$reply" in
    y|Y|yes|YES|Yes) : ;;
    *) err "license not accepted; installation aborted" ;;
  esac
}

# --- Target detection -------------------------------------------------------

OS="$(uname -s)"
ARCH="$(uname -m)"

detect_macos_target() {
  case "$ARCH" in
    arm64|aarch64) FEED_TARGET="darwin-aarch64"; DMG_ARCH="arm64" ;;
    x86_64) FEED_TARGET="darwin-x86_64"; DMG_ARCH="x64" ;;
    *) err "unsupported macOS architecture: $ARCH" ;;
  esac
}

detect_linux_target() {
  case "$ARCH" in
    x86_64|amd64) FEED_TARGET="linux-x86_64" ;;
    aarch64|arm64) FEED_TARGET="linux-aarch64" ;;
    *) err "unsupported Linux architecture: $ARCH" ;;
  esac
}

# --- macOS install ----------------------------------------------------------

install_macos() {
  detect_macos_target
  info "Installing Nobie for macOS ($FEED_TARGET, channel $CHANNEL)"

  version="$PIN_VERSION"
  if [ -z "$version" ]; then
    step "Fetching the latest version from the update feed"
    appcast_url="$FEED_BASE_MACOS/desktop-$FEED_TARGET-$CHANNEL.xml"
    appcast="$(http_get "$appcast_url")" || err "could not fetch appcast $appcast_url"
    # Tolerate an optional namespace prefix and open-tag attributes, matching the Rust parser.
    version="$(printf '%s' "$appcast" \
      | grep -oE '<[a-zA-Z:]*shortVersionString[^>]*>[^<]+' | head -n1 | sed 's/.*>//')"
    [ -n "$version" ] || err "could not read the latest version from $appcast_url"
  fi
  info "Latest version: $version"

  require_license

  dmg_url="$FEED_BASE_MACOS/desktop-$version/$FEED_TARGET/Nobie-$version-$DMG_ARCH.dmg"
  WORKDIR="$(mktemp -d)"
  tmp_dmg="$WORKDIR/Nobie.dmg"
  step "Downloading $dmg_url"
  download "$dmg_url" "$tmp_dmg" || err "failed to download the DMG"

  step "Mounting the disk image"
  # Feed "Y" to auto-agree the DMG's embedded license (already accepted above). $NF is the mount
  # point, which may contain spaces, so read it from the last tab-separated column.
  attach_out="$(printf 'Y\n' | hdiutil attach -nobrowse -noverify -noautoopen "$tmp_dmg" 2>/dev/null)" \
    || err "failed to mount the DMG"
  MOUNT_POINT="$(printf '%s\n' "$attach_out" | awk -F'\t' '/\/Volumes\// {print $NF; exit}')"
  [ -n "$MOUNT_POINT" ] || err "could not determine the DMG mount point"
  src_app="$MOUNT_POINT/$DMG_APP_NAME"
  [ -d "$src_app" ] || err "the DMG does not contain $DMG_APP_NAME"

  # Fail closed unless the bundle is validly signed and passes Gatekeeper — never install code we
  # cannot verify came from Nobie, even if the feed or transport is compromised.
  step "Verifying the app signature"
  codesign --verify --deep --strict "$src_app" >/dev/null 2>&1 \
    || err "the downloaded app failed code-signature verification; aborting"
  spctl --assess --type execute "$src_app" >/dev/null 2>&1 \
    || err "the downloaded app failed Gatekeeper assessment (notarization); aborting"

  apps_dir="$(dirname "$APP_PATH")"
  SUDO_APP="$(sudo_for "$apps_dir")"
  # `rm -rf`/`ditto` write into the existing bundle's nested dirs, so a root-owned prior install
  # under a user-writable /Applications still needs elevation.
  if [ -z "$SUDO_APP" ] && [ -e "$APP_PATH" ] && [ ! -w "$APP_PATH" ]; then
    have sudo || err "$APP_PATH is not writable and sudo is not available"
    SUDO_APP="sudo"
  fi
  if [ -n "$SUDO_APP" ]; then
    explain_sudo "install $DMG_APP_NAME into $apps_dir (replacing $APP_PATH)"
  fi

  step "Installing $DMG_APP_NAME into $apps_dir"
  if [ -e "$APP_PATH" ]; then
    $SUDO_APP rm -rf "$APP_PATH" || err "failed to remove the previous $DMG_APP_NAME"
  fi
  $SUDO_APP ditto "$src_app" "$APP_PATH" || err "failed to copy $DMG_APP_NAME into $apps_dir"
  $SUDO_APP xattr -dr com.apple.quarantine "$APP_PATH" >/dev/null 2>&1 || true

  # Detach the DMG now; the workdir is removed by the exit trap.
  if [ -n "$MOUNT_POINT" ]; then
    hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
    MOUNT_POINT=""
  fi

  install_wrapper

  ok "Installed Nobie $version to $APP_PATH"
  ok "The 'nobie' command is available at $WRAPPER_PATH"
}

install_wrapper() {
  bin_dir="$(dirname "$WRAPPER_PATH")"
  # Decide elevation up front so we can explain it before any sudo call.
  if [ -d "$bin_dir" ]; then
    SUDO_BIN="$(sudo_for "$bin_dir")"
  elif [ -w "$(dirname "$bin_dir")" ]; then
    SUDO_BIN=""
  else
    have sudo || err "$bin_dir does not exist and sudo is not available"
    SUDO_BIN="sudo"
  fi
  if [ -n "$SUDO_BIN" ]; then
    explain_sudo "install the nobie command into $bin_dir"
  fi
  if [ ! -d "$bin_dir" ]; then
    $SUDO_BIN mkdir -p "$bin_dir" || err "failed to create $bin_dir"
  fi
  step "Installing the nobie wrapper at $WRAPPER_PATH"
  [ -n "$WORKDIR" ] && [ -d "$WORKDIR" ] || WORKDIR="$(mktemp -d)"
  tmp_wrapper="$WORKDIR/nobie-wrapper"
  printf '#!/bin/sh\n# Auto-generated by the Nobie installer. Wraps the CLI bundled in Nobie.app.\nexec "%s/%s" "$@"\n' \
    "$APP_PATH" "$APP_CLI_RELPATH" >"$tmp_wrapper"
  $SUDO_BIN install -m 755 "$tmp_wrapper" "$WRAPPER_PATH" || err "failed to install the wrapper"
}

# --- Linux install ----------------------------------------------------------

install_linux() {
  detect_linux_target
  info "Installing Nobie for Linux ($FEED_TARGET, channel $CHANNEL)"
  [ -z "$PIN_VERSION" ] || err "--version is not supported on Linux yet"

  step "Fetching the release manifest"
  manifest_url="$FEED_BASE_LINUX/$CHANNEL/nobie-$FEED_TARGET.json"
  manifest="$(http_get "$manifest_url")" || err "could not fetch manifest $manifest_url"
  # The manifest is a small JSON object: {"version":..,"url":..,"sha256":..}.
  version="$(printf '%s' "$manifest" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
  url="$(printf '%s' "$manifest" | sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
  sha256="$(printf '%s' "$manifest" | sed -n 's/.*"sha256"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
  [ -n "$version" ] && [ -n "$url" ] && [ -n "$sha256" ] \
    || err "malformed release manifest at $manifest_url"
  info "Latest version: $version"

  require_license

  WORKDIR="$(mktemp -d)"
  tmp_bin="$WORKDIR/nobie"
  step "Downloading $url"
  download "$url" "$tmp_bin" || err "failed to download the binary"

  step "Verifying the download"
  # Compare digests case-insensitively so the installer and `nobie update` accept the same feed.
  actual="$(sha256_of "$tmp_bin" | tr '[:upper:]' '[:lower:]')"
  expected="$(printf '%s' "$sha256" | tr '[:upper:]' '[:lower:]')"
  [ "$actual" = "$expected" ] || err "integrity check failed: expected $sha256, got $actual"
  chmod 755 "$tmp_bin"

  # Replace an existing install wherever it lives; otherwise install to the default location.
  dest="$LINUX_BIN_PATH"
  if have nobie; then
    existing="$(command -v nobie)"
    [ -n "$existing" ] && dest="$existing"
  fi
  dest_dir="$(dirname "$dest")"
  # Decide elevation up front so we can explain it before any sudo call.
  if [ -d "$dest_dir" ]; then
    SUDO_BIN="$(sudo_for "$dest_dir")"
  elif [ -w "$(dirname "$dest_dir")" ]; then
    SUDO_BIN=""
  else
    have sudo || err "$dest_dir does not exist and sudo is not available"
    SUDO_BIN="sudo"
  fi
  if [ -n "$SUDO_BIN" ]; then
    explain_sudo "install nobie into $dest_dir"
  fi
  if [ ! -d "$dest_dir" ]; then
    $SUDO_BIN mkdir -p "$dest_dir" || err "failed to create $dest_dir"
  fi
  step "Installing nobie to $dest"
  # Stage alongside the target (same filesystem), then rename over it. Installing in place would
  # open the running binary and fail with "Text file busy"; rename just swaps the directory entry,
  # and any running process keeps executing the old inode until it exits.
  staged="$dest_dir/.nobie-install.$$"
  $SUDO_BIN install -m 755 "$tmp_bin" "$staged" || err "failed to stage the binary"
  $SUDO_BIN mv -f "$staged" "$dest" \
    || { $SUDO_BIN rm -f "$staged" >/dev/null 2>&1 || true; err "failed to install the binary"; }

  ok "Installed Nobie $version to $dest"
}

# --- Main -------------------------------------------------------------------

case "$OS" in
  Darwin) install_macos ;;
  Linux) install_linux ;;
  *) err "unsupported operating system: $OS" ;;
esac
