export PREDEV_MANIFEST_URL="${PREDEV_MANIFEST_URL:-https://api.pre.dev/api/tui/manifest}" export PREDEV_API_BASE="${PREDEV_API_BASE:-https://api.pre.dev}" export PREDEV_INSTALL_ID="${PREDEV_INSTALL_ID:-8995c82c-2aea-4504-b7d0-9b4745ce032d}" case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*) if command -v powershell.exe >/dev/null 2>&1; then echo "pre.dev: this is Windows, so installing the Windows build with PowerShell..." PREDEV_ARGS="$*" PREDEV_NO_AUTORUN=1 powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "irm 'https://api.pre.dev/install.ps1' | iex" rc=$? [ "$rc" -eq 0 ] && echo "Done. Open a new terminal and run: predev" exit "$rc" fi echo "pre.dev: run this in PowerShell instead: irm https://api.pre.dev/install.ps1 | iex" >&2 if [ "${PREDEV_NO_TELEMETRY:-}" != "1" ]; then curl -fsS --max-time 5 -X POST "$PREDEV_API_BASE/api/tui/installed" -H "Content-Type: application/json" \ -H "x-predev-install-id: $PREDEV_INSTALL_ID" \ -d '{"ok":false,"stage":"platform","error":"windows shell without powershell.exe"}' >/dev/null 2>&1 || true fi exit 1 ;; esac #!/bin/bash # pre.dev TUI installer — served at https://pre.dev/install: # # curl -fsSL https://pre.dev/install | bash # curl -fsSL https://pre.dev/install | bash -s -- open [--code ] [--editor cursor|code] # # Arguments after `--` go to predev when it launches, so the second form is # the web's "Open on your computer": install if missing, then `predev open`. # Already installed at the current version, it skips the download. --code is # the web's one-time sign-in (predev trades it for a saved login); it is never # printed. # # Installs the launcher to ~/.predev/bin/predev and the newest binary to # ~/.predev/versions//. The launcher handles updates from then on — # this script is a one-time bootstrap. # # PREDEV_MANIFEST_URL overrides the manifest endpoint (used by CI smoke tests # against a local server before the public endpoint exists). set -euo pipefail # Default = the BACKEND host (api.pre.dev). pre.dev is the static frontend and # 404s on /api/* — with the old default the LAUNCHER's background update check # (which runs without PREDEV_MANIFEST_URL set) silently never updated anyone. MANIFEST_URL="${PREDEV_MANIFEST_URL:-https://api.pre.dev/api/tui/manifest}" API_BASE="${PREDEV_API_BASE:-https://api.pre.dev}" ROOT="${PREDEV_HOME:-$HOME/.predev}" BIN_DIR="$ROOT/bin" VERSIONS_DIR="$ROOT/versions" # ── install telemetry ───────────────────────────────────────────────────── # GET /install mints PREDEV_INSTALL_ID and exports it above this line. It ties # the curl to the binary download, to this script finishing, to the user who # eventually logs in. Persisted so the launcher and the binary can echo it. # PREDEV_NO_TELEMETRY=1 opts out of everything below (same flag as the TUI). INSTALL_ID="${PREDEV_INSTALL_ID:-}" STARTED_MS=$(date +%s000) REPORTED=0 # report [error] — fire-and-forget outcome ping. Never fails the # install: backgrounded, output discarded, short timeout, `|| true`. report() { [[ "${PREDEV_NO_TELEMETRY:-}" == "1" ]] && return 0 [[ "$REPORTED" == "1" ]] && return 0 REPORTED=1 local ok="$1" stage="$2" err="${3:-}" local now; now=$(date +%s000) (curl -fsS --max-time 5 -X POST "$API_BASE/api/tui/installed" \ -H 'Content-Type: application/json' \ -H "x-predev-install-id: ${INSTALL_ID}" \ -H "x-predev-platform: ${PLATFORM:-}" \ -d "{\"install_id\":\"${INSTALL_ID}\",\"ok\":${ok},\"stage\":\"${stage}\",\"error\":\"${err//\"/}\",\"version\":\"${VERSION:-}\",\"platform\":\"${PLATFORM:-}\",\"trigger\":\"install\",\"duration_ms\":$((now - STARTED_MS)),\"shell\":\"$(basename "${SHELL:-unknown}")\"}" \ >/dev/null 2>&1 || true) & disown 2>/dev/null || true } # Any early exit (set -e, an explicit exit 1, a killed curl) reports a failure # with the stage it died at. The success path calls report() first, and REPORTED # makes this a no-op after that. ONE exit trap for the whole script — the temp # binary cleanup lives here too, because a second `trap ... EXIT` would silently # replace this one and we'd lose every failure report. STAGE="start" TMP="" on_exit() { local code=$? [[ -n "$TMP" ]] && rm -f "$TMP" report false "$STAGE" "exit $code" } trap on_exit EXIT # ── platform detection ──────────────────────────────────────────────────── STAGE="platform" OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$ARCH" in arm64|aarch64) ARCH=arm64 ;; x86_64|amd64) ARCH=x64 ;; *) echo "unsupported architecture: $ARCH" >&2; report false "platform" "unsupported arch $ARCH"; exit 1 ;; esac case "$OS" in darwin|linux) ;; *) echo "unsupported OS: $OS (macOS and Linux only)" >&2; report false "platform" "unsupported os $OS"; exit 1 ;; esac PLATFORM="${OS}-${ARCH}" VEC_EXT="dylib"; [[ "$OS" == "linux" ]] && VEC_EXT="so" echo "pre.dev TUI installer — ${PLATFORM}" # ── fetch manifest ──────────────────────────────────────────────────────── STAGE="manifest" command -v python3 >/dev/null || { echo "python3 required" >&2; report false "manifest" "python3 missing"; exit 1; } MANIFEST=$(curl -fsSL \ -H "x-predev-install-id: ${INSTALL_ID}" \ -H "x-predev-platform: ${PLATFORM}" \ "$MANIFEST_URL") read -r VERSION URL SHA <&2 report false "manifest" "no manifest entry for $PLATFORM" exit 1 fi # ── download + verify + install ─────────────────────────────────────────── STAGE="download" DEST="$VERSIONS_DIR/$VERSION" mkdir -p "$DEST" "$BIN_DIR" # Portable digest: minimal Linux images (Debian slim, Alpine, most Docker # bases) ship sha256sum but NOT shasum (perl) — the bare `shasum` call here # exit-127'd 2 of the first 3 real installs (PostHog, 2026-08-10). sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}'; fi } # This exact binary is already here (the web's one command, run by someone who # has pre.dev): skip the download. The sha check still guards a partial file. FRESH=1 if [[ -x "$DEST/predev" && "$(sha256_of "$DEST/predev")" == "$SHA" ]]; then echo "pre.dev v${VERSION} is already installed" FRESH=0 else echo "installing v${VERSION}" TMP=$(mktemp) curl -fsSL --progress-bar \ -H "x-predev-install-id: ${INSTALL_ID}" \ -H "x-predev-platform: ${PLATFORM}" \ "$URL" -o "$TMP" STAGE="verify" GOT_SHA=$(sha256_of "$TMP") if [[ "$GOT_SHA" != "$SHA" ]]; then echo "sha256 mismatch — refusing to install (expected $SHA, got $GOT_SHA)" >&2 report false "verify" "sha256 mismatch" exit 1 fi mv "$TMP" "$DEST/predev" chmod +x "$DEST/predev" TMP="" fi STAGE="sidecars" # Persist the install id so the launcher's update checks and the binary's own # events carry it — that's what joins this anonymous install to a real user # once they log in. [[ -n "$INSTALL_ID" ]] && printf '%s\n' "$INSTALL_ID" > "$ROOT/install_id" 2>/dev/null || true # vec0 sidecar (native sqlite extension; best-effort — TUI works without it # until a feature needs local vector search). VEC_URL="${URL%/*}/vec0-${PLATFORM}.${VEC_EXT}" [[ -s "$DEST/vec0.${VEC_EXT}" ]] || curl -fsSL "$VEC_URL" -o "$DEST/vec0.${VEC_EXT}" 2>/dev/null || true # Browser driver (playwright-core) — must live as a REAL on-disk install: the # compiled binary's bundled copy can't read its own package.json/registry on # any machine but the build host. Best-effort here (background, npm required); # the TUI self-installs on first browser use if this was skipped. Version must # match PW_VERSION in local_browser_ops.ts. if command -v npm >/dev/null && [[ ! -f "$ROOT/pw/node_modules/playwright-core/index.js" ]]; then (npm install --no-save --prefix "$ROOT/pw" playwright-core@1.60.0 >/dev/null 2>&1 || true) & disown 2>/dev/null || true fi # Versioned launch logic — ALL launcher behavior lives in the release-shipped # launch.sh (asset `predev-launch`), so it updates through releases like the # binary. The bin/predev below is a permanent TRAMPOLINE that never changes. curl -fsSL "${URL%/*}/predev-launch" -o "$DEST/launch.sh" 2>/dev/null && chmod +x "$DEST/launch.sh" || true # ── trampoline ──────────────────────────────────────────────────────────── cat > "$BIN_DIR/predev" <<'LAUNCHER' #!/bin/bash # predev-trampoline v1 — do not edit. Execs the NEWEST version's launch.sh # (all real launch logic ships with each release); falls back to exec'ing the # binary directly for version dirs installed before launch.sh existed. The # binary self-heals this file if it predates the trampoline. set -euo pipefail ROOT="${PREDEV_HOME:-$HOME/.predev}" VERSIONS_DIR="$ROOT/versions" CURRENT=$(ls -1 "$VERSIONS_DIR" 2>/dev/null | grep -v '^\.' | sort -V | tail -1) if [[ -z "$CURRENT" ]]; then echo "no installed version — run: curl -fsSL https://pre.dev/install | bash" >&2 exit 1 fi if [[ -x "$VERSIONS_DIR/$CURRENT/launch.sh" ]]; then exec "$VERSIONS_DIR/$CURRENT/launch.sh" "$@" fi # pre-launch.sh version dir — minimal legacy env so the binary still runs export PREDEV_TUI_VERSION="$CURRENT" export PREDEV_TURN_URL="${PREDEV_TURN_URL:-https://predev-agent.onrender.com}" export PREDEV_API_BASE="${PREDEV_API_BASE:-https://api.pre.dev}" for ext in dylib so; do [[ -f "$VERSIONS_DIR/$CURRENT/vec0.$ext" ]] && export SQLITE_VEC_DYLIB="$VERSIONS_DIR/$CURRENT/vec0.$ext" done exec "$VERSIONS_DIR/$CURRENT/predev" "$@" LAUNCHER chmod +x "$BIN_DIR/predev" if [[ "$FRESH" == "1" ]]; then echo "" echo "✓ installed pre.dev TUI v${VERSION} → $BIN_DIR/predev" fi # The binary is on disk and the trampoline is written — this is the real # "installed" moment. Reported BEFORE the PATH section so a weird shell rc can # never turn a working install into a reported failure. STAGE="path" case ":$PATH:" in *":$BIN_DIR:"*) ON_PATH=true ;; *) ON_PATH=false ;; esac report_success() { [[ "${PREDEV_NO_TELEMETRY:-}" == "1" ]] && return 0 REPORTED=1 local now; now=$(date +%s000) (curl -fsS --max-time 5 -X POST "$API_BASE/api/tui/installed" \ -H 'Content-Type: application/json' \ -H "x-predev-install-id: ${INSTALL_ID}" \ -H "x-predev-platform: ${PLATFORM}" \ -d "{\"install_id\":\"${INSTALL_ID}\",\"ok\":true,\"stage\":\"done\",\"version\":\"${VERSION}\",\"platform\":\"${PLATFORM}\",\"trigger\":\"install\",\"duration_ms\":$((now - STARTED_MS)),\"shell\":\"$(basename "${SHELL:-unknown}")\",\"on_path\":${ON_PATH}}" \ >/dev/null 2>&1 || true) & disown 2>/dev/null || true } report_success # ── PATH setup (automatic, idempotent) ──────────────────────────────────── # Append the export line to the user's shell rc so `predev` just works in the # next shell — same pattern as bun/deno/uv installers. Skips if already on # PATH or already appended; prints the manual line as a fallback. PATH_LINE="export PATH=\"$BIN_DIR:\$PATH\"" add_to_rc() { local rc="$1" [[ -f "$rc" ]] || touch "$rc" 2>/dev/null || return 1 grep -qsF "$BIN_DIR" "$rc" && return 0 # already there printf '\n# pre.dev TUI\n%s\n' "$PATH_LINE" >> "$rc" echo " added to $rc" } case ":$PATH:" in *":$BIN_DIR:"*) ;; # already on PATH — nothing to do *) SHELL_NAME=$(basename "${SHELL:-}") ADDED=0 case "$SHELL_NAME" in zsh) add_to_rc "${ZDOTDIR:-$HOME}/.zshrc" && ADDED=1 ;; bash) # macOS login shells read .bash_profile; Linux reads .bashrc. if [[ "$OS" == "darwin" ]]; then add_to_rc "$HOME/.bash_profile" && ADDED=1 else add_to_rc "$HOME/.bashrc" && ADDED=1; fi ;; fish) FISH_DIR="$HOME/.config/fish/conf.d" if mkdir -p "$FISH_DIR" 2>/dev/null; then printf 'fish_add_path %s\n' "$BIN_DIR" > "$FISH_DIR/predev.fish" echo " added to $FISH_DIR/predev.fish"; ADDED=1 fi ;; esac echo "" if [[ "$ADDED" == "1" ]]; then echo "restart your shell (or run: source your rc file), then:" else echo "add it to your PATH, then:" echo " echo '$PATH_LINE' >> ~/.zshrc && source ~/.zshrc" fi ;; esac echo "" # ── auto-launch ─────────────────────────────────────────────────────────── # One command → coding agent running. `curl | bash` keeps stdin as the pipe, # so hand the TUI the real terminal via /dev/tty (test that, not -t 0). Skips # in CI, when there is no usable tty (docker RUN, provisioning scripts), or # with PREDEV_NO_AUTORUN=1 — those get the old "run: predev" hint. exec is the # LAST line bash reads from the pipe, so replacing the shell is safe, and the # success report + PATH setup above have already happened. The script's own # arguments (`bash -s -- open `) go to predev. if [[ "${PREDEV_NO_AUTORUN:-}" != "1" && -z "${CI:-}" && -t 1 && -r /dev/tty ]]; then if [[ "${1:-}" == "open" ]]; then echo "opening the project in pre.dev (ctrl-c to exit; next time just run: predev)" else echo "launching predev — your terminal becomes the coding agent (ctrl-c to exit; next time just run: predev)" fi echo "" # This script just read the manifest: the launcher skips its own check. PREDEV_UPDATE_CHECKED=1 exec "$BIN_DIR/predev" "$@" < /dev/tty fi # The hint never repeats a sign-in code: it is single-use, and this output # may land in a CI log. HINT=() while [[ $# -gt 0 ]]; do case "$1" in --code) shift 2 || shift ;; --code=*) shift ;; *) HINT+=("$1"); shift ;; esac done echo "run: predev${HINT[*]:+ ${HINT[*]}}"