diff --git a/apps/logicsrc-web/public/install.sh b/apps/logicsrc-web/public/install.sh index c5b5ffb..939513d 100755 --- a/apps/logicsrc-web/public/install.sh +++ b/apps/logicsrc-web/public/install.sh @@ -10,8 +10,10 @@ # # What it does: # 1. Detects OS (Linux/macOS — Windows: use WSL) and requires Node 18+. -# 2. Fetches the repo tarball from GitHub into $LOGICSRC_HOME/src. -# 3. `npm install` + `npm run build:cli` (builds only the CLI's workspaces). +# 2. Fetches the repo tarball from GitHub into a staging dir. +# 3. `npm install` + `npm run build:cli` (builds only the CLI's workspaces) +# there, then swaps it into $LOGICSRC_HOME/src only once it built. A failed +# run leaves any existing install untouched. # 4. Drops a `logicsrc` wrapper on $HOME/.local/bin. # # Env overrides: @@ -60,8 +62,15 @@ check_node() { # bare text, so this needs no jq. Empty on failure — never fatal, since a missing # sha only costs `logicsrc update` its precision. resolve_sha() { - curl -fsSL -H "Accept: application/vnd.github.sha" \ - "https://api.github.com/repos/$GH_REPO/commits/$LOGICSRC_REF" 2>/dev/null || true + _sha="$(curl -fsSL -H "Accept: application/vnd.github.sha" \ + "https://api.github.com/repos/$GH_REPO/commits/$LOGICSRC_REF" 2>/dev/null || true)" + # Anything that is not a commit id is dropped rather than recorded: an error + # page or a proxy's HTML would otherwise be written into install.json as the + # commit, and `logicsrc update` compares against that string. + case "$_sha" in + *[!0-9a-f]* | "") echo "" ;; + *) [ "${#_sha}" = 40 ] && echo "$_sha" || echo "" ;; + esac } # Records what we installed so `logicsrc update` can compare against the remote. @@ -79,22 +88,70 @@ write_manifest() { EOF } +# Everything is built in a staging directory and only swapped in once it works, +# so a failed or interrupted run cannot leave a half-installed tree behind. +# +# It used to `rm -rf "$SRC_DIR"` before building. If the build then failed -- +# or the run was interrupted, or the machine went to sleep -- you were left with +# no dist/, a wrapper still pointing at it, and a manifest from the previous +# install claiming success. Every later `logicsrc` invocation died with +# MODULE_NOT_FOUND and nothing said why. +# +# Staging lives inside $LOGICSRC_HOME rather than /tmp so the swap is a rename +# on the same filesystem, not a cross-device copy of node_modules. +STAGE="$LOGICSRC_HOME/.staging.$$" +PREV="$LOGICSRC_HOME/.previous.$$" +BUILD_LOG="${TMPDIR:-/tmp}/logicsrc-install.$$.log" + +cleanup_stage() { rm -rf "$STAGE" "$PREV"; } + +# Build output is captured rather than discarded: "build failed" with no reason +# is not a diagnosis. The log survives so it can be read or pasted. +step_fail() { + printf '%s ✗%s %s\n' "$R" "$X" "$1" >&2 + if [ -s "$BUILD_LOG" ]; then + printf '\n%s--- last 25 lines ---%s\n' "$Y" "$X" >&2 + tail -n 25 "$BUILD_LOG" >&2 + printf '%s--- full log: %s ---%s\n' "$Y" "$BUILD_LOG" "$X" >&2 + fi + printf '\nyour existing install was left untouched.\n' >&2 + cleanup_stage + exit 1 +} + do_install() { detect_os; check_node need curl; need tar + trap cleanup_stage INT TERM HUP + info "fetching logicsrc@$LOGICSRC_REF from GitHub…" - mkdir -p "$SRC_DIR" + mkdir -p "$LOGICSRC_HOME" sha="$(resolve_sha)" short_sha="$(printf '%.7s' "$sha")" - tmp="$(mktemp -d)" - curl -fsSL "$TARBALL_URL" | tar -xz -C "$tmp" --strip-components=1 - rm -rf "$SRC_DIR"; mkdir -p "$(dirname "$SRC_DIR")"; mv "$tmp" "$SRC_DIR" - ok "downloaded to $SRC_DIR${short_sha:+ ($short_sha)}" + rm -rf "$STAGE"; mkdir -p "$STAGE" + curl -fsSL "$TARBALL_URL" | tar -xz -C "$STAGE" --strip-components=1 \ + || step_fail "download failed — could not fetch $TARBALL_URL" + ok "downloaded${short_sha:+ ($short_sha)}" info "installing dependencies (this can take a minute)…" - ( cd "$SRC_DIR" && npm install --no-audit --no-fund --ignore-scripts >/dev/null 2>&1 ) || fail "npm install failed — run it by hand in $SRC_DIR" + ( cd "$STAGE" && npm install --no-audit --no-fund --ignore-scripts ) >"$BUILD_LOG" 2>&1 \ + || step_fail "npm install failed" info "building the CLI…" - ( cd "$SRC_DIR" && npm run build:cli >/dev/null 2>&1 ) || fail "build failed — run 'npm run build:cli' in $SRC_DIR" + ( cd "$STAGE" && npm run build:cli ) >>"$BUILD_LOG" 2>&1 \ + || step_fail "build failed" + + # The wrapper execs this exact file, so its absence is the failure the user + # would otherwise only discover on their next command. + [ -f "$STAGE/packages/cli/dist/index.js" ] \ + || step_fail "build produced no packages/cli/dist/index.js" + + # Swap. This is the first point at which a working install is touched. + rm -rf "$PREV" + [ -d "$SRC_DIR" ] && mv "$SRC_DIR" "$PREV" + mv "$STAGE" "$SRC_DIR" || { [ -d "$PREV" ] && mv "$PREV" "$SRC_DIR"; step_fail "could not move the build into $SRC_DIR"; } + rm -rf "$PREV" + rm -f "$BUILD_LOG" + trap - INT TERM HUP mkdir -p "$LOGICSRC_BIN" cat > "$WRAPPER" <