#!/bin/sh
# coop user-local installer. No sudo, global npm install, or shell-profile changes.
set -eu
umask 022
VERSION='0.7.1'
ARCHIVE_SHA256='b4fd465c00a29436e6d6c613f7db59c8ffc09b5da509e157e69ee6ec9ca6d099'
NODE_VERSION='v24.21.0'
INSTALL_DIR=${COOP_INSTALL_DIR:-"$HOME/.local/share/coop"}
BIN_DIR=${COOP_BIN_DIR:-"$HOME/.local/bin"}
DOWNLOAD_BASE=${COOP_DOWNLOAD_BASE:-https://coop.grokked.it}
MANAGED_NODE=false
INTEGRATIONS=true
while [ "$#" -gt 0 ]; do
case "$1" in
  --managed-node) MANAGED_NODE=true ;;
  --no-integrations) INTEGRATIONS=false ;;
  --help) printf '%s\n' 'Usage: sh install.sh [--managed-node] [--no-integrations]' 'Installs coop under ~/.local/share/coop and commands under ~/.local/bin.' 'Override with COOP_INSTALL_DIR and COOP_BIN_DIR. macOS and Linux: arm64/x64.'; exit 0 ;;
  *) printf '%s\n' 'Unknown option. Use --help.' >&2; exit 1 ;;
esac
shift
done
case "$INSTALL_DIR:$BIN_DIR" in *'
'*) printf '%s\n' 'Installation paths cannot contain newlines.' >&2; exit 1;; esac
case "$DOWNLOAD_BASE" in https://*|http://127.0.0.1:*|http://localhost:*) ;; *) printf '%s\n' 'Download URL requires HTTPS.' >&2; exit 1;; esac
command -v tar >/dev/null || { printf '%s\n' 'tar is required.' >&2; exit 1; }
fetch() {
  if command -v curl >/dev/null 2>&1; then curl --fail --silent --show-error --location --max-time 300 --output "$2" "$1"
  elif command -v wget >/dev/null 2>&1; then wget -q --timeout=300 -O "$2" "$1"
  else printf '%s\n' 'Install curl or wget first.' >&2; exit 1; fi
}
checksum() {
  if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d ' ' -f 1
  elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d ' ' -f 1
  else printf '%s\n' 'sha256sum or shasum is required.' >&2; exit 1; fi
}
verify() { [ "$(checksum "$1")" = "$2" ] || { printf '%s\n' 'Checksum mismatch. Installation stopped; existing installation is unchanged.' >&2; exit 1; }; }
mkdir -p "$INSTALL_DIR" "$BIN_DIR"
INSTALL_DIR=$(cd "$INSTALL_DIR" && pwd)
BIN_DIR=$(cd "$BIN_DIR" && pwd)
mkdir "$INSTALL_DIR/.install-lock" 2>/dev/null || { printf '%s\n' 'Another install is running (or a previous install left .install-lock).' >&2; exit 1; }
STAGE=''
cleanup() { [ -z "$STAGE" ] || rm -rf "$STAGE"; rmdir "$INSTALL_DIR/.install-lock" 2>/dev/null || true; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
STAGE=$(mktemp -d "$INSTALL_DIR/.install.XXXXXX")
printf 'Installing coop %s…\n' "$VERSION"
fetch "${DOWNLOAD_BASE%/}/releases/coop-$VERSION.tar.gz" "$STAGE/app.tar.gz"
verify "$STAGE/app.tar.gz" "$ARCHIVE_SHA256"
NODE_BINARY=''
if [ "$MANAGED_NODE" = false ] && command -v node >/dev/null 2>&1 && node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 24 ? 0 : 1)' >/dev/null 2>&1; then
  NODE_BINARY=$(node -p 'process.execPath')
else
  case "$(uname -s)" in Darwin) NODE_OS=darwin ;; Linux) NODE_OS=linux ;; *) printf '%s\n' 'Supported systems: macOS and Linux.' >&2; exit 1;; esac
  case "$(uname -m)" in arm64|aarch64) NODE_ARCH=arm64 ;; x86_64|amd64) NODE_ARCH=x64 ;; *) printf '%s\n' 'Supported CPUs: arm64 and x64.' >&2; exit 1;; esac
  case "$NODE_OS-$NODE_ARCH" in
    darwin-arm64) NODE_SHA256='bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057' ;;
    darwin-x64) NODE_SHA256='1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097' ;;
    linux-arm64) NODE_SHA256='724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5' ;;
    linux-x64) NODE_SHA256='6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff' ;;
  esac
  NODE_NAME="node-$NODE_VERSION-$NODE_OS-$NODE_ARCH"
  printf 'Downloading private Node.js %s runtime…\n' "$NODE_VERSION"
  fetch "https://nodejs.org/dist/$NODE_VERSION/$NODE_NAME.tar.gz" "$STAGE/node.tar.gz"
  verify "$STAGE/node.tar.gz" "$NODE_SHA256"
  tar -xzf "$STAGE/node.tar.gz" -C "$STAGE"
  mkdir -p "$INSTALL_DIR/runtime"
  if [ ! -d "$INSTALL_DIR/runtime/$NODE_NAME" ]; then mv "$STAGE/$NODE_NAME" "$INSTALL_DIR/runtime/$NODE_NAME"; fi
  NODE_BINARY="$INSTALL_DIR/runtime/$NODE_NAME/bin/node"
fi
"$NODE_BINARY" -e 'if (Number(process.versions.node.split(".")[0]) < 24) process.exit(1)'
tar -xzf "$STAGE/app.tar.gz" -C "$STAGE"
"$NODE_BINARY" "$STAGE/coop/dist/cli.js" --version >/dev/null
# Use Node for atomic symlink replacement and correct quoting of arbitrary user paths.
"$NODE_BINARY" --input-type=module - "$INSTALL_DIR" "$BIN_DIR" "$STAGE/coop" "$VERSION" "$NODE_BINARY" "$ARCHIVE_SHA256" <<'JS'
import { mkdirSync, existsSync, readFileSync, writeFileSync, renameSync, symlinkSync, lstatSync, readlinkSync, unlinkSync, rmSync } from 'node:fs';
import { join } from 'node:path';
const [root, bin, staged, version, node, checksum] = process.argv.slice(2);
const quote = value => "'" + value.replaceAll("'", "'\\''") + "'";
for (const name of ['coop']) {
  const target = join(bin, name);
  try {
    const stat = lstatSync(target);
    if (!stat.isSymbolicLink() || readlinkSync(target) !== join(root, 'bin', name)) throw new Error(`Refusing to overwrite unrelated command: ${target}`);
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
}
mkdirSync(join(root, 'releases'), { recursive: true }); mkdirSync(join(root, 'bin'), { recursive: true });
const release = join(root, 'releases', version);
if (existsSync(release)) {
  if (readFileSync(join(release, '.archive-sha256'), 'utf8') !== checksum) throw new Error('This version is installed with a different checksum; use a new release version.');
  rmSync(staged, { recursive: true });
} else { writeFileSync(join(staged, '.archive-sha256'), checksum); renameSync(staged, release); }
const next = join(root, `.current-${process.pid}`); symlinkSync(release, next); renameSync(next, join(root, 'current'));
for (const name of ['coop']) {
  const wrapper = join(root, 'bin', name);
  writeFileSync(wrapper, `#!/bin/sh\nexec ${quote(node)} ${quote(join(root, 'current', 'dist', 'cli.js'))} "$@"\n`, { mode: 0o755 });
  const target = join(bin, name); try { unlinkSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
  symlinkSync(wrapper, target);
}
JS
printf '\nInstalled: %s/coop\n' "$BIN_DIR"
printf 'Add this directory to PATH if necessary: %s\n' "$BIN_DIR"
printf '%s\n' 'Quick start: coop init --name "My team" --owner "Your name"; coop serve' 'ChatGPT models additionally require Codex on PATH and codex login on the main.' 'No workspace data or shell profiles were changed.'

if [ "$INTEGRATIONS" = true ] && ( : </dev/tty ) 2>/dev/null; then
  "$NODE_BINARY" "$INSTALL_DIR/current/dist/cli.js" setup </dev/tty >/dev/tty || printf '%s\n' 'Coop is installed. Integration setup did not complete; retry with coop setup.'
else
  printf '%s\n' 'To install the skill and register MCP later, run: coop setup'
fi
