#!/bin/sh
# Miblo launcher: finds a Node.js >= 20 (or downloads a pinned one, once) and runs one of the
# scripts next to this file with it.
#
#   sh miblo-run [--no-wait] <script> [args...]
#   sh miblo-run --check [--data <dir>]
#
# Every hook, the status line and the slash commands go through it: apps started from the
# desktop (the Claude desktop app, IDEs) don't inherit the shell's PATH, so `node` is often not
# found there, and some users have no Node at all. Plain POSIX sh: /bin/sh on macOS and Linux,
# Git Bash's sh on Windows (Claude Code runs hooks there).
#
# Lookup, cheapest first: the cached path; `node` on PATH; well-known install locations
# (Homebrew, Volta, nvm, fnm, asdf, mise, nodenv, /usr/bin); a node downloaded earlier; the
# user's login shell. The hit is cached in <data>/runtime/node-path. When nothing is found,
# the official Node LTS below is downloaded into <data>/runtime/, its SHA-256 checked against
# the hash pinned here, and unpacked. With --no-wait (hooks Claude Code waits for, and the
# status line) nothing is downloaded: it exits 0 with no output.
#
# --check (for the slash commands) prints one line and runs nothing: `ok version=<v> node=<path>`
# (exit 0) or `missing reason=<why>` (exit 1). It downloads Node when needed, with progress on
# stderr.
#
# It never breaks Claude Code: on any failure it exits 0 quietly and writes the reason to
# <data>/runtime/launcher.log. <data> is $CLAUDE_PLUGIN_DATA, else the plugin's data dir under
# the Claude config dir when it exists, else ~/.miblo. Only --check and the status line take
# `--data <absolute dir>` (right after them): the slash commands' arguments carry user text, which
# must never choose the node that runs. For --check it must be one of those canonical dirs (or
# ~/.miblo); any other is ignored.
#
# Trust: PATH entries that are empty or relative are skipped; a node found inside the working
# directory runs but is never cached; the cache is used only when it is ours (owned by the user,
# in a runtime dir nobody else can write, naming an absolute .../node that still runs as >= 20);
# a downloaded runtime likewise (a regular file, it and its dirs owned by the user and private).
#
# Environment for tests: MIBLO_SYSROOT (prefix of the system locations), MIBLO_NODE_PLATFORM
# (<os>-<arch>), MIBLO_WAIT (seconds to wait for a download another process is making, default
# 120); with MIBLO_TEST=1 only, MIBLO_UNAME (stands in for `uname -s`), MIBLO_NODE_BASE_URL (a mirror; file:// allowed) and
# MIBLO_NODE_SHA256 (the expected hash). Downloads are https only: curl with --proto =https
# --proto-redir =https, wget with --https-only --max-redirect=0 (nodejs.org/dist does not
# redirect).

MIN_MAJOR=20
NODE_VERSION=24.21.0

# SHA-256 of node-v$NODE_VERSION-<target>.<ext>, from nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.
pinned_sha() {
  case $1 in
    darwin-arm64) echo bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057 ;;
    darwin-x64) echo 1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097 ;;
    linux-x64) echo 6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff ;;
    linux-arm64) echo 724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5 ;;
    win-x64) echo 158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541 ;;
    win-arm64) echo 8779b1bde1d39f8d420e3b57aa657b39891af434d3de44a919044cec06785921 ;;
    *) return 1 ;;
  esac
}

case $0 in
  */*) here=${0%/*} ;;
  *) here=. ;;
esac

nowait=0
check=0
case ${1:-} in
  --no-wait)
    nowait=1
    shift
    ;;
  --check)
    check=1
    shift
    ;;
esac
if [ "$check" = 0 ]; then
  script=${1:-}
  [ $# -gt 0 ] && shift
  # Only a script that sits next to the launcher.
  case $script in
    '' | .* | */* | *\\*) exit 0 ;;
  esac
  [ -f "$here/$script" ] || exit 0
fi

conf=${CLAUDE_CONFIG_DIR:-${HOME:-.}/.claude}

# One spelling of a folder, to compare data dirs: no doubled or trailing slash. Under Git Bash
# (MINGW/MSYS/CYGWIN) also: the same folder comes as C:\x (Claude Code's CLAUDE_PLUGIN_DATA), C:/x
# (the installer) or /c/x (Git Bash's HOME), and all become /c/x (cygpath when there is one), the
# drive letter in lower case. Elsewhere a backslash is an ordinary character in a name.
# Asked only on this path (uname is a process): whether this is Git Bash.
git_bash() {
  gb_os=$(uname -s 2> /dev/null)
  [ "${MIBLO_TEST:-}" = 1 ] && [ -n "${MIBLO_UNAME:-}" ] && gb_os=$MIBLO_UNAME
  case $gb_os in
    MINGW* | MSYS* | CYGWIN*) windows=1 ;;
    *) windows=0 ;;
  esac
}
canon() {
  cn_p=$1
  if [ "$windows" = 1 ]; then
    case $cn_p in
      [A-Za-z]:*)
        # Git Bash's own, by its absolute path: never one found through PATH.
        if [ -x /usr/bin/cygpath ]; then
          cn_p=$(/usr/bin/cygpath -u -- "$cn_p" 2> /dev/null) || cn_p=$1
        fi
        ;;
    esac
    while :; do
      case $cn_p in
        *\\*) cn_p="${cn_p%%\\*}/${cn_p#*\\}" ;;
        *) break ;;
      esac
    done
    case $cn_p in
      [A-Za-z]:/* | [A-Za-z]:)
        cn_d=$(printf '%s' "${cn_p%%:*}" | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
        cn_p="/$cn_d${cn_p#?:}"
        ;;
    esac
  fi
  while :; do
    case $cn_p in
      *//*) cn_p="${cn_p%%//*}/${cn_p#*//}" ;;
      *) break ;;
    esac
  done
  while :; do
    case $cn_p in
      ?*/) cn_p=${cn_p%/} ;;
      *) break ;;
    esac
  done
  printf '%s\n' "$cn_p"
}

data=
if [ "$check" = 1 ] || [ "${script:-}" = statusline-tap.mjs ]; then
  if [ "${1:-}" = "--data" ]; then
    case ${2:-} in
      /* | [A-Za-z]:[\\/]*) data=$2 ;;
    esac
    # --check is pre-allowed in the slash commands, where a prompt injection could steer it to a
    # cache planted in a cloned repository: there, only the canonical data dirs count.
    if [ "$check" = 1 ] && [ -n "$data" ]; then
      # A match takes the canonical dir as spelt there, never the caller's spelling.
      git_bash
      cd_given=$(canon "$data")
      data=
      for cd_dir in "${CLAUDE_PLUGIN_DATA:-}" "$conf/plugins/data/miblo-miblo" "${HOME:+$HOME/.miblo}"; do
        if [ -n "$cd_dir" ] && [ "$(canon "$cd_dir")" = "$cd_given" ]; then
          data=$cd_dir
          break
        fi
      done
    fi
  fi
fi
[ -n "$data" ] || data=${CLAUDE_PLUGIN_DATA:-}
if [ -z "$data" ]; then
  if [ -d "$conf/plugins/data/miblo-miblo" ]; then
    data="$conf/plugins/data/miblo-miblo"
  else
    data="${HOME:-.}/.miblo"
  fi
fi
rt="$data/runtime"
cache="$rt/node-path"
sysroot=${MIBLO_SYSROOT:-}
nocache=0
# Git Bash: Windows' FIND.EXE may come first on PATH.
FIND=find
[ -x /usr/bin/find ] && FIND=/usr/bin/find

fail_reason="Node.js not found"
too_old=

# Logs a failure and keeps it as the reason --check reports.
fail() {
  fail_reason=$*
  log "$*"
}

say() {
  [ "$check" = 1 ] && echo "Miblo: $*" >&2
  return 0
}

log() {
  mkdir -p "$rt" 2>/dev/null || return 0
  if [ -f "$rt/launcher.log" ] && [ "$(wc -c < "$rt/launcher.log" 2>/dev/null || echo 0)" -gt 65536 ]; then
    mv -f "$rt/launcher.log" "$rt/launcher.log.old" 2>/dev/null
  fi
  printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null)" "$*" >> "$rt/launcher.log" 2>/dev/null
  return 0
}

remember() {
  mkdir -p "$rt" 2>/dev/null || return 0
  printf '%s\n' "$1" > "$cache.$$" 2>/dev/null && mv -f "$cache.$$" "$cache" 2>/dev/null
  rm -f "$cache.$$" 2>/dev/null
  return 0
}

# A file that runs and reports a version >= MIN_MAJOR.
node_ok() {
  [ -f "$1" ] && [ -x "$1" ] || return 1
  ok_v=$("$1" -v 2>/dev/null) || return 1
  ok_v=${ok_v#v}
  ok_major=${ok_v%%.*}
  case $ok_major in
    '' | *[!0-9]*) return 1 ;;
  esac
  if [ "$ok_major" -lt "$MIN_MAJOR" ]; then
    too_old="$1 is v$ok_v, too old ($MIN_MAJOR or newer needed)"
    return 1
  fi
  node_version=v$ok_v
  return 0
}

found() {
  fd_node=$1
  shift
  [ "$nocache" = 1 ] || remember "$fd_node"
  if [ "$check" = 1 ]; then
    echo "ok version=$node_version node=$fd_node"
    exit 0
  fi
  # exec replaces this shell: the script's exit status and output are Claude Code's.
  exec "$fd_node" "$here/$script" "$@"
}

try() {
  if node_ok "$1"; then
    node=$1
    return 0
  fi
  return 1
}

# Of paths shaped .../vX.Y.Z<suffix>, the executable one with the highest version.
newest() {
  nw_suffix=$1
  shift
  newest_path=
  nw_best=-1
  for nw_p in "$@"; do
    [ -x "$nw_p" ] || continue
    nw_v=${nw_p%"$nw_suffix"}
    nw_v=${nw_v##*/}
    nw_v=${nw_v#v}
    nw_maj=${nw_v%%.*}
    nw_rest=${nw_v#*.}
    nw_min=${nw_rest%%.*}
    nw_pat=${nw_rest#*.}
    nw_pat=${nw_pat%%[!0-9]*}
    case "$nw_maj$nw_min$nw_pat" in
      '' | *[!0-9]*) continue ;;
    esac
    nw_n=$((nw_maj * 1000000 + nw_min * 1000 + nw_pat))
    if [ "$nw_n" -gt "$nw_best" ]; then
      nw_best=$nw_n
      newest_path=$nw_p
    fi
  done
  [ -n "$newest_path" ]
}

# The known install locations a node may live in even inside the working directory.
known_place() {
  case $1 in
    "$HOME"/.nvm/* | "$HOME"/.volta/* | "$HOME"/.fnm/* | "$HOME"/.local/share/fnm/* | "$HOME"/.asdf/* | \
      "$HOME"/.local/share/mise/* | "$HOME"/.nodenv/* | "$HOME/Library/Application Support/fnm"/* | \
      /opt/homebrew/* | /usr/local/* | /usr/bin/*) return 0 ;;
  esac
  return 1
}

# `command -v node`, without its traps: empty, "." and relative PATH entries would run a node
# planted in whatever repository Claude Code was started in.
path_node() {
  pn_cwd=$(pwd -P 2> /dev/null) || pn_cwd=
  pn_ifs=$IFS
  IFS=:
  set -f
  for pn_d in $PATH; do
    case $pn_d in
      /* | [A-Za-z]:[\\/]*) ;;
      *) continue ;;
    esac
    for pn_n in node node.exe; do
      [ -f "$pn_d/$pn_n" ] || continue
      IFS=$pn_ifs
      if try "$pn_d/$pn_n"; then
        # Inside the working directory (resolved: no ./, .. or symlink tricks): run it, never cache it.
        pn_real=$(cd "$pn_d" 2> /dev/null && pwd -P) || pn_real=$pn_d
        case $pn_real/ in
          "$pn_cwd"/*) [ -n "$pn_cwd" ] && ! known_place "$pn_real/" && nocache=1 ;;
        esac
        set +f
        return 0
      fi
      IFS=:
    done
  done
  IFS=$pn_ifs
  set +f
  return 1
}

# The cache, only when it is plainly ours.
cache_usable() {
  [ -f "$cache" ] && [ -O "$cache" ] || return 1
  [ -z "$("$FIND" "$rt" -prune \( -perm -020 -o -perm -002 \) 2> /dev/null)" ] || return 1
  node=
  IFS= read -r node < "$cache" || [ -n "$node" ] || return 1
  case $node in
    /* | [A-Za-z]:[\\/]*) ;;
    *) return 1 ;;
  esac
  case ${node##*/} in
    node | node.exe) ;;
    *) return 1 ;;
  esac
  try "$node"
}

well_known() {
  try "$sysroot/opt/homebrew/bin/node" && return 0
  try "$sysroot/usr/local/bin/node" && return 0
  try "$HOME/.volta/bin/node" && return 0
  nvm_dir=${NVM_DIR:-$HOME/.nvm}
  newest /bin/node "$nvm_dir"/versions/node/v*/bin/node && try "$newest_path" && return 0
  for fnm_dir in "${FNM_DIR:-$HOME/.local/share/fnm}" "$HOME/.fnm" "$HOME/Library/Application Support/fnm"; do
    newest /installation/bin/node "$fnm_dir"/node-versions/v*/installation/bin/node && try "$newest_path" && return 0
  done
  try "$HOME/.asdf/shims/node" && return 0
  try "$HOME/.local/share/mise/shims/node" && return 0
  try "$HOME/.nodenv/shims/node" && return 0
  try "$sysroot/usr/bin/node" && return 0
  # Windows (Git Bash)
  try "$sysroot/c/Program Files/nodejs/node.exe" && return 0
  [ -n "${NVM_SYMLINK:-}" ] && try "$NVM_SYMLINK/node.exe" && return 0
  [ -n "${LOCALAPPDATA:-}" ] && try "$LOCALAPPDATA/Volta/bin/node.exe" && return 0
  return 1
}

# Owned by the user and not writable by group or others.
private() {
  [ -O "$1" ] && [ -z "$("$FIND" "$1" -prune \( -perm -020 -o -perm -002 \) 2> /dev/null)" ]
}

# A runtime downloaded earlier, only when it is plainly ours, as the cache: a regular file, and
# it and every directory up to the runtime dir private.
downloaded() {
  for dl in "$rt"/node-v*/bin/node "$rt"/node-v*/node.exe; do
    [ -f "$dl" ] && [ ! -L "$dl" ] || continue
    dl_p=$dl
    while [ "$dl_p" != "$rt" ] && private "$dl_p"; do
      dl_p=${dl_p%/*}
    done
    [ "$dl_p" = "$rt" ] && private "$rt" || continue
    try "$dl" && return 0
  done
  return 1
}

# The login shell sets up PATH from .zprofile/.profile (Homebrew, most version managers).
# Not interactive (-i could grab the terminal Claude Code runs in); bounded to ~2 s; a miss is
# remembered for an hour so the status line doesn't ask on every refresh.
login_shell() {
  [ -n "${SHELL:-}" ] && [ -x "$SHELL" ] || return 1
  miss="$rt/no-login-node"
  if [ -f "$miss" ] && [ -n "$("$FIND" "$miss" -mmin -60 2>/dev/null)" ]; then
    return 1
  fi
  mkdir -p "$rt" 2>/dev/null || return 1
  ls_out="$rt/login.$$"
  "$SHELL" -lc 'command -v node' < /dev/null > "$ls_out" 2> /dev/null &
  ls_pid=$!
  ls_i=0
  while kill -0 "$ls_pid" 2> /dev/null; do
    ls_i=$((ls_i + 1))
    if [ "$ls_i" -gt 20 ]; then
      kill "$ls_pid" 2> /dev/null
      break
    fi
    sleep 0.1 2> /dev/null || sleep 1
  done
  wait "$ls_pid" 2> /dev/null
  ls_hit=
  while IFS= read -r ls_line; do
    case $ls_line in
      /*) ls_hit=$ls_line ;;
    esac
  done < "$ls_out"
  rm -f "$ls_out"
  if [ -n "$ls_hit" ] && try "$ls_hit"; then
    rm -f "$miss"
    return 0
  fi
  : > "$miss"
  return 1
}

platform() {
  if [ -n "${MIBLO_NODE_PLATFORM:-}" ]; then
    target=$MIBLO_NODE_PLATFORM
  else
    case $(uname -s 2> /dev/null) in
      Darwin) pf_os=darwin ;;
      Linux) pf_os=linux ;;
      MINGW* | MSYS* | CYGWIN*) pf_os=win ;;
      *) return 1 ;;
    esac
    case $(uname -m 2> /dev/null) in
      x86_64 | amd64) pf_arch=x64 ;;
      arm64 | aarch64) pf_arch=arm64 ;;
      *) return 1 ;;
    esac
    # A shell under Rosetta reports x86_64 on Apple silicon: take the native build.
    if [ "$pf_os" = darwin ] && [ "$(sysctl -n hw.optional.arm64 2> /dev/null)" = 1 ]; then
      pf_arch=arm64
    fi
    # The official Linux builds need glibc.
    if [ "$pf_os" = linux ]; then
      for musl in /lib/ld-musl-*; do
        [ -e "$musl" ] && return 1
      done
    fi
    target="$pf_os-$pf_arch"
  fi
  want=$(pinned_sha "$target") || return 1
  [ "${MIBLO_TEST:-}" = 1 ] && [ -n "${MIBLO_NODE_SHA256:-}" ] && want=$MIBLO_NODE_SHA256
  name="node-v$NODE_VERSION-$target"
  case $target in
    win-*) ext=zip exe="$rt/$name/node.exe" ;;
    *) ext=tar.gz exe="$rt/$name/bin/node" ;;
  esac
  return 0
}

sha256_of() {
  if command -v shasum > /dev/null 2>&1; then
    sh_out=$(shasum -a 256 "$1" 2> /dev/null)
  elif command -v sha256sum > /dev/null 2>&1; then
    sh_out=$(sha256sum "$1" 2> /dev/null)
  elif command -v openssl > /dev/null 2>&1; then
    sh_out=$(openssl dgst -sha256 -r "$1" 2> /dev/null)
  else
    return 1
  fi
  set -- $sh_out
  [ -n "${1:-}" ] || return 1
  printf '%s\n' "$1" | tr 'A-F' 'a-f'
}

unpack() {
  case $ext in
    zip)
      if command -v unzip > /dev/null 2>&1; then
        unzip -q "$1" -d "$2"
      elif [ -n "${SYSTEMROOT:-}" ] && [ -x "$SYSTEMROOT/System32/tar.exe" ]; then
        "$SYSTEMROOT/System32/tar.exe" -xf "$1" -C "$2"
      else
        return 1
      fi
      ;;
    *) tar -xzf "$1" -C "$2" ;;
  esac
}

# Download, verify and unpack. Holds $rt/lock (a directory: mkdir is atomic) meanwhile.
fetch() {
  base=https://nodejs.org/dist
  protos='=https'
  if [ "${MIBLO_TEST:-}" = 1 ]; then
    base=${MIBLO_NODE_BASE_URL:-$base}
    protos='=https,file'
  fi
  url="$base/v$NODE_VERSION/$name.$ext"
  part="$rt/$name.$ext.part.$$"
  stage="$rt/stage.$$"
  log "downloading $url"
  say "Downloading Node.js v$NODE_VERSION (about 50 MB, only this once)..."
  if command -v curl > /dev/null 2>&1; then
    # nodejs.org/dist answers directly; a redirect may only go to https.
    curl -fsSL --proto "$protos" --proto-redir "$protos" --connect-timeout 15 --max-time 300 -o "$part" "$url" 2>> "$rt/launcher.log"
  elif command -v wget > /dev/null 2>&1; then
    wget --https-only --max-redirect=0 -q -T 30 -O "$part" "$url" 2>> "$rt/launcher.log"
  else
    fail "download failed: neither curl nor wget found"
    return 1
  fi
  if [ $? -ne 0 ] || [ ! -s "$part" ]; then
    fail "download failed: $url"
    rm -f "$part"
    return 1
  fi
  got=$(sha256_of "$part") || {
    fail "cannot verify the download: no shasum, sha256sum or openssl"
    rm -f "$part"
    return 1
  }
  if [ "$got" != "$want" ]; then
    fail "checksum mismatch for $name.$ext: got $got, want $want"
    rm -f "$part"
    return 1
  fi
  rm -rf "$stage"
  mkdir -p "$stage" && unpack "$part" "$stage" > /dev/null 2>> "$rt/launcher.log"
  fx_status=$?
  rm -f "$part"
  if [ "$fx_status" -ne 0 ] || [ ! -d "$stage/$name" ]; then
    fail "unpacking $name.$ext failed"
    rm -rf "$stage"
    return 1
  fi
  rm -rf "${rt:?}/${name:?}"
  mv "$stage/$name" "$rt/$name" || {
    fail "cannot move $name into place"
    rm -rf "$stage"
    return 1
  }
  rm -rf "$stage"
  log "installed $name"
  say "Node.js v$NODE_VERSION installed in $rt/$name."
  return 0
}

lock_is_stale() {
  lk_pid=
  [ -f "$rt/lock/pid" ] && IFS= read -r lk_pid < "$rt/lock/pid"
  # Its owner died (killed by a hook timeout), or it is older than any download may take.
  if [ -n "$lk_pid" ] && ! kill -0 "$lk_pid" 2> /dev/null; then
    return 0
  fi
  [ -n "$("$FIND" "$rt/lock" -prune -mmin +10 2> /dev/null)" ]
}

# Removes the download lock only while it is still this process's: one taken over (wrongly
# judged stale) and re-made by another hook is left to that hook.
release_lock() {
  rl_pid=
  [ -f "$rt/lock/pid" ] && IFS= read -r rl_pid < "$rt/lock/pid"
  [ "$rl_pid" = "$$" ] && rm -rf "$rt/lock"
  return 0
}

install_runtime() {
  platform || {
    fail "no official Node.js build for this platform"
    return 1
  }
  mkdir -p "$rt" 2> /dev/null || return 1
  # Offline: hooks run on every event, so after a failure only --check tries again within 10 min.
  failed="$rt/download-failed"
  if [ "$check" = 0 ] && [ -f "$failed" ] && [ -n "$("$FIND" "$failed" -mmin -10 2> /dev/null)" ]; then
    fail_reason="the Node.js download failed less than 10 minutes ago"
    return 1
  fi
  wait_s=${MIBLO_WAIT:-120}
  waited=0
  until mkdir "$rt/lock" 2> /dev/null; do
    if lock_is_stale; then
      # Atomic takeover: of several processes seeing the same stale lock, one mv wins. The lock
      # moved may not be the one judged stale (re-made in between) or its owner may still be
      # downloading: then it goes back (unless a new one is already there) and this hook waits.
      if mv "$rt/lock" "$rt/lock.stale.$$" 2> /dev/null; then
        st_pid=
        [ -f "$rt/lock.stale.$$/pid" ] && IFS= read -r st_pid < "$rt/lock.stale.$$/pid"
        if [ -n "$st_pid" ] && kill -0 "$st_pid" 2> /dev/null; then
          if [ ! -e "$rt/lock" ] && mv "$rt/lock.stale.$$" "$rt/lock" 2> /dev/null; then
            :
          else
            rm -rf "$rt/lock.stale.$$"
          fi
        else
          rm -rf "$rt/lock.stale.$$"
          continue
        fi
      else
        continue
      fi
    fi
    # Another hook is downloading: use its result once it is there.
    if try "$exe"; then
      return 0
    fi
    if [ "$waited" -ge "$wait_s" ]; then
      fail "another process is still downloading Node.js; try again in a minute"
      return 1
    fi
    sleep 1
    waited=$((waited + 1))
  done
  echo $$ > "$rt/lock/pid"
  trap 'release_lock' EXIT
  trap 'exit 1' HUP INT TERM
  in_status=0
  if ! try "$exe"; then
    if fetch && try "$exe"; then
      rm -f "$failed"
    else
      : > "$failed"
      in_status=1
    fi
  fi
  release_lock
  trap - EXIT HUP INT TERM
  [ "$in_status" -eq 0 ] || {
    [ -d "$rt/$name" ] && fail "the downloaded $name does not run"
    return 1
  }
  return 0
}

if cache_usable; then
  [ "$check" = 1 ] && found "$node"
  exec "$node" "$here/$script" "$@"
fi

node=
path_node && found "$node" "$@"
nocache=0
well_known && found "$node" "$@"
downloaded && found "$node" "$@"
login_shell && found "$node" "$@"

if [ "$nowait" = 1 ]; then
  exit 0
fi
if install_runtime; then
  found "$node" "$@"
fi
[ -n "$too_old" ] && fail_reason="$fail_reason; $too_old"
if [ "$check" = 1 ]; then
  echo "missing reason=$fail_reason"
  exit 1
fi
echo "Miblo: Node.js was not found and could not be downloaded (see $rt/launcher.log)." >&2
exit 0
