#!/usr/bin/env bash
# Generic wrapper for running commands inside nix develop.
# Symlink to this file with the name of the command you want to wrap.
# Example: ln -s nix-wrapper node
#
# Uses "nix develop path:DIR" so flake.nix does not need to be git-tracked.
#
# This file is vendored independently per-project -- there is no central
# sync mechanism. Fixes made here (or found stale here) must be manually
# propagated to every other project's bin/nix-wrapper. Check for drift with:
#   for f in ~/projects/*/bin/nix-wrapper; do md5sum "$f"; done | sort | uniq -c -w32

set -eu

# Find flake.nix in `start` or any ancestor, return its directory.
find_flake_dir() {
  local dir="$1"
  while [[ "$dir" != "/" ]]; do
    [[ -f "$dir/flake.nix" ]] && { echo "$dir"; return 0; }
    dir=$(dirname "$dir")
  done
  return 1
}

main() {
  local asCalled folder prog flakeDir
  asCalled=$(basename "$0")
  folder=$(cd "$(dirname "$0")"; pwd)

  # Find the real program, excluding our bin/ directory
  # May be empty for commands that only exist inside nix (node, npm)
  prog=$(type -Pa "$asCalled" | grep -v "^$folder/" | head -1)

  # If the tool already resolves outside this wrapper, use it directly.
  # Covers both "not in a nix shell" and "already in an impure shell that
  # happens to provide this tool" -- no IN_NIX_SHELL check needed here.
  if [[ -n $prog ]]; then
    exec "$prog" "$@"
  fi

  # prog is empty: not reachable outside this wrapper. Try a flake for the
  # CURRENT directory (not whatever shell we may already be inside -- that
  # shell has already been ruled out above, since prog came up empty even
  # while running in it). Deliberately no IN_NIX_SHELL=impure short-circuit:
  # that check assumed the current impure shell provides every tool, which
  # is false when cd'ing into a project needing a different flake (#65767).
  #
  # Fall back to the wrapper's OWN directory's flake (folder's parent chain)
  # when $PWD's ancestor search comes up empty -- callers that `cd` away from
  # the project entirely (e.g. a test's scratch tmpdir) still want THIS
  # project's devShell, not a failure, since the wrapper symlink itself lives
  # inside the project that should provide the tool (finances #75244).
  if flakeDir=$(find_flake_dir "$PWD") || flakeDir=$(find_flake_dir "$folder"); then
    flakeDir=$(realpath "$flakeDir")

    # Re-entry guard (jeeves #128507): nix develop is not idempotent on
    # re-entry -- each level layers the devShell's PATH on top of the
    # inherited environment rather than detecting "already in this exact
    # devShell." NIX_WRAPPER_DEVSHELLS is a colon-delimited ANCESTRY LIST
    # (not a single last-entered marker) of every canonicalized flakeDir
    # this shell lineage has already entered via this wrapper -- a scalar
    # marker only catches immediate self-re-entry (A->A) and misses an
    # interleaved A->B->A cycle, which still stacks A's PATH a second time.
    # Membership is tested via `grep -qF` (literal substring, not a glob
    # pattern) -- a `case`/glob membership test mis-evaluates any flakeDir
    # containing shell glob metacharacters (*, ?, [) as pattern syntax
    # instead of literal text, producing false membership results. grep -F
    # sidesteps this entirely. The colon delimiter itself remains a
    # disclosed (not engineered-around) residual -- a flakeDir containing a
    # literal ':' could in theory defeat the boundary check, but nix flake
    # dirs in this ecosystem are always plain filesystem paths under the
    # user's own project trees, where a literal colon would already break
    # numerous other tools; accepted as practically unreachable rather than
    # solved with a length-prefixed encoding.
    #
    # Residual, disclosed rather than assumed away: this is an
    # env-var-propagation-based guard, not a kernel-enforced one -- it is
    # defeated by deliberate environment scrubbing between levels (env -i,
    # sudo without -E, a shellHook that unsets or overwrites
    # NIX_WRAPPER_DEVSHELLS). Same disclosed-residual shape as this
    # ecosystem's other env-propagated markers (era's EVTCTL_HOOK_IDENTITY,
    # evtctl.md "Mux identity detail"). This is wrapper-internal state --
    # manually exporting NIX_WRAPPER_DEVSHELLS is a misuse, not a supported
    # interface.
    if [[ $flakeDir == *:* || $flakeDir == *$'\n'* ]]; then
      echo "$asCalled: flakeDir '$flakeDir' contains a colon or newline -- cannot safely track ancestry, refusing (jeeves #128507)" >&2
      exit 127
    fi

    if grep -qF ":$flakeDir:" <<<":${NIX_WRAPPER_DEVSHELLS:-}:"; then
      echo "$asCalled: already inside nix develop for $flakeDir (ancestry: ${NIX_WRAPPER_DEVSHELLS:-none}) but $asCalled is not on PATH -- refusing to re-enter (would stack PATH; see jeeves #128507)" >&2
      exit 127
    fi

    exec env NIX_WRAPPER_DEVSHELLS="${NIX_WRAPPER_DEVSHELLS:+$NIX_WRAPPER_DEVSHELLS:}$flakeDir" nix develop "path:$flakeDir" -c "$asCalled" "$@"
  fi

  # No prog, no flake -- nothing left to try. Fail cleanly instead of
  # exec'ing an empty/self-resolving string: `exec "${prog:-$asCalled}"`
  # here infinite-loops, because $asCalled re-resolves through PATH back to
  # this same wrapper in the same shell (era a4f61198ea98).
  echo "$asCalled: command not found (no flake providing it in $PWD or any ancestor)" >&2
  exit 127
}

main "$@"
