fix: delegate linux-amd64/arm64 to jerboa-shell-extras when extras features requested

ober

9679e26a75018a6f1c70ff9deb8fc757b7152bb3

diff --git a/.gitignore b/.gitignore
index b9fc3d7..fef5e3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,4 @@
 /.jcode/
 /.claude/
 .gerbil-lsp-cache/
+vendor/jerboa-shell-extras/
diff --git a/.jerbuild.linux-amd64 b/.jerbuild.linux-amd64
index 33b9335..0ce1a36 100644
--- a/.jerbuild.linux-amd64
+++ b/.jerbuild.linux-amd64
@@ -11,7 +11,7 @@
 
 (cc "cc")
 
-(os-libs "-lm -ldl -lpthread -ltinfo")
+(os-libs "-lm -ldl -lpthread -ltinfo -ludev")
 
 (libdirs "_jerbuild-stage"   ; stdlib patches — must shadow bundle copies
          "src"
@@ -33,7 +33,8 @@
   "_jerbuild-stage/libjerboa_native.a"
   "_jerbuild-stage/libjsh_coreutils.a"
   "_jerbuild-stage/libjerboa_ssh.a"
-  "_jerbuild-stage/libjerboa_fuse.a")
+  "_jerbuild-stage/libjerboa_fuse.a"
+  "_jerbuild-stage/libyubikey_native.a")
 
 ;; --export-dynamic + Sforeign_symbol registrations in main.c are how Chez
 ;; foreign-procedure finds symbols. --allow-multiple-definition tolerates
diff --git a/Makefile b/Makefile
index b62731a..5ac810b 100644
--- a/Makefile
+++ b/Makefile
@@ -126,7 +126,8 @@ build: binary
 
 ensure-jerboa-tools:
 	@set -e; \
-	if [ -n "$(JERBUILD)" ] && [ -x "$(JERBUILD)" ] && "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then \
+	_jb_cmd="$$(echo '$(JERBUILD)' | awk '{print $$1}')"; \
+	if [ -n "$(JERBUILD)" ] && [ -x "$$_jb_cmd" ] && $(JERBUILD) --jerboa-home >/dev/null 2>&1; then \
 	  echo "=== Using Jerboa toolchain: $(JERBUILD) ==="; \
 	else \
 	  echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \
@@ -146,6 +147,8 @@ ensure-shell-extras:
 		git -C "$(JERBOA_SHELL_EXTRAS_DIR)" pull --ff-only; \
 	fi
 	@test -f "$(JERBOA_SHELL_EXTRAS_DIR)/Makefile"
+	@# Patch extras repo's build config for yubikey/udev support
+	sh support/patch-extras-build.sh "$(JERBOA_SHELL_EXTRAS_DIR)"
 
 configure: ensure-shell-extras
 	@$(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" configure \
@@ -539,18 +542,36 @@ yubikey-native-linux-arm64: rust-musl-target-arm64
 	        --release --target aarch64-unknown-linux-musl --no-default-features
 	@test -f $(XC_YUBIKEY_NATIVE_MUSL_ARM64_LIB)
 
-linux-amd64: ensure-shell-extras stage-jsqlite ffi-shim-symbols.list vendor-deps-linux jerboa gen-embed chez-cross-amd64 rust-coreutils-linux-amd64 jerboa-native-linux-amd64 vendor-staged-libs yubikey-native-linux-amd64
-	@echo "=== Wiping stale per-arch compiled artifacts (.so/.wpo/boot) ==="
-	@$(XC_WIPE)
+linux-amd64: ensure-shell-extras vendor-deps-linux
 	@features="$(JSH_CROSS_FEATURES)"; \
 	    if [ -f "$(JERBOA_SHELL_EXTRAS_DIR)/Makefile" ] && echo "$$features" | grep -qE '^(all|.*embed|.*pass|.*wireguard|.*harden|.*awk|.*sed)'; then \
 	        echo "=== Extras features detected — delegating to jerboa-shell-extras ==="; \
-	        $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" linux-amd64 \
-	            JERBOA_SHELL_DIR="$(CURDIR)" \
-	            JSH_CROSS_FEATURES="$(JSH_CROSS_FEATURES)" \
-	            JSH_FEATURES="$(JSH_FEATURES)"; \
-	        cp -f "$(JERBOA_SHELL_EXTRAS_DIR)/jsh-linux-amd64" "$(CURDIR)/jsh-linux-amd64"; \
+	        case "$$(uname -s)-$$(uname -m)" in \
+	          Linux-x86_64|Linux-amd64) \
+	            echo "=== Native Linux build via jerboa-shell-extras ==="; \
+	            $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" jsh-linux-native \
+	                JERBOA_SHELL_DIR="$(CURDIR)" \
+	                JSH_FEATURES="$(JSH_FEATURES)"; \
+	            cp -f "$(JERBOA_SHELL_EXTRAS_DIR)/jsh-linux-amd64" "$(CURDIR)/jsh-linux-amd64"; ;; \
+	          *) \
+	            echo "=== Cross-build via jerboa-shell-extras (macOS -> Linux) ==="; \
+	            $(MAKE) chez-cross-amd64 jerboa-native-linux-amd64 yubikey-native-linux-amd64 rust-coreutils-linux-amd64; \
+	            JERBOA_HOME=$(abspath $(JERBOA_HOME)) JSH_CROSS_FEATURES="$(JSH_CROSS_FEATURES)" \
+	              $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" _linux-amd64-delegated \
+	                JERBOA_SHELL_DIR="$(abspath $(CURDIR))" \
+	                JSH_FEATURES="$(JSH_FEATURES)" \
+	                JSH_CROSS_FEATURES="$(JSH_CROSS_FEATURES)" \
+	                XC_NATIVE_RS_DIR="$(abspath $(XC_NATIVE_RS_DIR))" \
+	                XC_NATIVE_MUSL_AMD64_LIB="$(abspath $(XC_NATIVE_MUSL_AMD64_LIB))" \
+	                XC_YUBIKEY_NATIVE_MUSL_AMD64_LIB="$(abspath $(XC_YUBIKEY_NATIVE_MUSL_AMD64_LIB))"; \
+	                XC_YUBIKEY_NATIVE_MUSL_AMD64_LIB="$(XC_YUBIKEY_NATIVE_MUSL_AMD64_LIB)"; \
+	            cp -f "$(JERBOA_SHELL_EXTRAS_DIR)/jsh-linux-amd64" "$(CURDIR)/jsh-linux-amd64"; ;; \
+	        esac \
 	    else \
+	        echo "=== Building cross-compilation prerequisites ==="; \
+	        $(MAKE) stage-jsqlite ffi-shim-symbols.list jerboa gen-embed chez-cross-amd64 rust-coreutils-linux-amd64 jerboa-native-linux-amd64 vendor-staged-libs yubikey-native-linux-amd64; \
+	        echo "=== Wiping stale per-arch compiled artifacts (.so/.wpo/boot) ==="; \
+	        $(XC_WIPE); \
 	        echo "=== Cross-building jsh-linux-amd64 (x86_64 Linux static, JSH_FEATURES=$$features) ==="; \
 	        JERBOA_HOME=$(JERBOA_HOME) TARGET_ARCH=amd64 JSH_FEATURES="$$features" JERBOA_NATIVE_FEATURES="$(JERBOA_NATIVE_FEATURES)" JERBOA_NATIVE_A="$(XC_NATIVE_MUSL_AMD64_LIB)" YUBIKEY_NATIVE_A="$(XC_YUBIKEY_NATIVE_MUSL_AMD64_LIB)" $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jsh-cross.ss; \
 	    fi
@@ -558,18 +579,32 @@ linux-amd64: ensure-shell-extras stage-jsqlite ffi-shim-symbols.list vendor-deps
 	@ls -lh jsh-linux-amd64
 	@file jsh-linux-amd64
 
-linux-arm64: ensure-shell-extras stage-jsqlite ffi-shim-symbols.list vendor-deps-linux jerboa gen-embed chez-cross-arm64 rust-coreutils-linux-arm64 jerboa-native-linux-arm64 vendor-staged-libs yubikey-native-linux-arm64
-	@echo "=== Wiping stale per-arch compiled artifacts (.so/.wpo/boot) ==="
-	@$(XC_WIPE)
+linux-arm64: ensure-shell-extras vendor-deps-linux
 	@features="$(JSH_CROSS_FEATURES)"; \
 	    if [ -f "$(JERBOA_SHELL_EXTRAS_DIR)/Makefile" ] && echo "$$features" | grep -qE '^(all|.*embed|.*pass|.*wireguard|.*harden|.*awk|.*sed)'; then \
 	        echo "=== Extras features detected — delegating to jerboa-shell-extras ==="; \
-	        $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" linux-arm64 \
-	            JERBOA_SHELL_DIR="$(CURDIR)" \
-	            JSH_CROSS_FEATURES="$(JSH_CROSS_FEATURES)" \
-	            JSH_FEATURES="$(JSH_FEATURES)"; \
-	        cp -f "$(JERBOA_SHELL_EXTRAS_DIR)/jsh-linux-arm64" "$(CURDIR)/jsh-linux-arm64"; \
+	        case "$$(uname -s)-$$(uname -m)" in \
+	          Linux-aarch64|Linux-arm64) \
+	            echo "=== Native Linux build via jerboa-shell-extras ==="; \
+	            $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" jsh-linux-native \
+	                JERBOA_SHELL_DIR="$(CURDIR)" \
+	                JSH_FEATURES="$(JSH_FEATURES)"; \
+	            cp -f "$(JERBOA_SHELL_EXTRAS_DIR)/jsh-linux-arm64" "$(CURDIR)/jsh-linux-arm64"; ;; \
+	          *) \
+	            echo "=== Cross-build via jerboa-shell-extras (macOS -> Linux) ==="; \
+	            $(MAKE) chez-cross-arm64; \
+	            JERBOA_HOME=$(JERBOA_HOME) JSH_CROSS_FEATURES="$(JSH_CROSS_FEATURES)" \
+	              $(MAKE) -C "$(JERBOA_SHELL_EXTRAS_DIR)" _linux-arm64-delegated \
+	                JERBOA_SHELL_DIR="$(CURDIR)" \
+	                JSH_FEATURES="$(JSH_FEATURES)" \
+	                XC_NATIVE_RS_DIR="$(XC_NATIVE_RS_DIR)" \
+	                XC_NATIVE_MUSL_ARM64_LIB="$(XC_NATIVE_MUSL_ARM64_LIB)" \
+	                XC_YUBIKEY_NATIVE_MUSL_ARM64_LIB="$(XC_YUBIKEY_NATIVE_MUSL_ARM64_LIB)"; \
 	    else \
+	        echo "=== Building cross-compilation prerequisites ==="; \
+	        $(MAKE) stage-jsqlite ffi-shim-symbols.list jerboa gen-embed chez-cross-arm64 rust-coreutils-linux-arm64 jerboa-native-linux-arm64 vendor-staged-libs yubikey-native-linux-arm64; \
+	        echo "=== Wiping stale per-arch compiled artifacts (.so/.wpo/boot) ==="; \
+	        $(XC_WIPE); \
 	        echo "=== Cross-building jsh-linux-arm64 (aarch64 Linux static, JSH_FEATURES=$$features) ==="; \
 	        JERBOA_HOME=$(JERBOA_HOME) TARGET_ARCH=arm64 JSH_FEATURES="$$features" JERBOA_NATIVE_FEATURES="$(JERBOA_NATIVE_FEATURES)" JERBOA_NATIVE_A="$(XC_NATIVE_MUSL_ARM64_LIB)" YUBIKEY_NATIVE_A="$(XC_YUBIKEY_NATIVE_MUSL_ARM64_LIB)" $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jsh-cross.ss; \
 	    fi
diff --git a/support/patch-extras-build.sh b/support/patch-extras-build.sh
new file mode 100755
index 0000000..868e46d
--- /dev/null
+++ b/support/patch-extras-build.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# patch-extras-build.sh — Apply yubikey/udev patches to extras repo build config.
+# Called by ensure-shell-extras from the main repo's Makefile.
+set -eu
+
+EXTRAS_DIR="${1:-}"
+if [ -z "$EXTRAS_DIR" ]; then
+    echo "Usage: $0 <jerboa-shell-extras-dir>" >&2
+    exit 1
+fi
+
+# Patch .jerbuild.linux-amd64: add yubikey archive
+JERBUILD_CONFIG="$EXTRAS_DIR/.jerbuild.linux-amd64"
+if [ -f "$JERBUILD_CONFIG" ]; then
+    grep -q 'libyubikey_native' "$JERBUILD_CONFIG" 2>/dev/null || \
+        perl -i -pe 's/(_jerbuild-stage\/libjerboa_fuse\.a)/$1\n  "_jerbuild-stage\/libyubikey_native.a"/' "$JERBUILD_CONFIG"
+    perl -i -pe 's/(-ltinfo)"/$1 -ludev"/' "$JERBUILD_CONFIG"
+fi
+
+# Patch stage-for-jerbuild-linux.sh: add yubikey staging section
+STAGE_SCRIPT="$EXTRAS_DIR/support/stage-for-jerbuild-linux.sh"
+if [ -f "$STAGE_SCRIPT" ]; then
+    grep -q 'yubikey-native' "$STAGE_SCRIPT" 2>/dev/null || \
+        cat >> "$STAGE_SCRIPT" << 'PATCH_EOF'
+
+# ── yubikey-native Rust archive ────────────────────────────────────────
+YUBIKEY_NATIVE_DIR="${VENDOR}/jerboa-yubikey/yubikey-native"
+YUBIKEY_NATIVE_AR_OUT="${STAGE}/libyubikey_native.a"
+if [ -f "${YUBIKEY_NATIVE_DIR}/target/release/libyubikey_native.a" ]; then
+    echo "==> Staging yubikey-native archive -> ${YUBIKEY_NATIVE_AR_OUT}"
+    cp -f "${YUBIKEY_NATIVE_DIR}/target/release/libyubikey_native.a" "${YUBIKEY_NATIVE_AR_OUT}"
+else
+    echo "==> yubikey-native not found; creating empty ${YUBIKEY_NATIVE_AR_OUT}"
+    [ -n "$(command -v ar || command -v llvm-ar || true)" ] && \
+        "$(command -v ar || command -v llvm-ar)" rcs "${YUBIKEY_NATIVE_AR_OUT}" || true
+fi
+PATCH_EOF
+fi
\ No newline at end of file
diff --git a/support/stage-for-jerbuild-linux.sh b/support/stage-for-jerbuild-linux.sh
index 0431057..f20524f 100755
--- a/support/stage-for-jerbuild-linux.sh
+++ b/support/stage-for-jerbuild-linux.sh
@@ -87,6 +87,30 @@ find "${STAGE}/jsqlite" -type f \( -name '*.so' -o -name '*.wpo' \
 # libdir entry shadows the vendor copy.
 patch_into "${REPO_ROOT}/patches/sed-pcre2.sls" "${STAGE}/sed/pcre2.sls"
 
+# ── [3b/5] Use extras repo's jsh.ss if available (embeds extras commands) ──
+EXTRAS_JSH="${VENDOR}/jerboa-shell-extras/jsh.ss"
+EXTRAS_JSH_SRC="${VENDOR}/jerboa-shell-extras/jerboa-src/src/jsh"
+if [ -f "${EXTRAS_JSH}" ]; then
+    echo "==> [3b/5] Using extras jsh.ss from ${EXTRAS_JSH}"
+    cp -f "${EXTRAS_JSH}" "${REPO_ROOT}/jsh.ss"
+    echo "==> [3c/5] Copying extras jsh modules from ${EXTRAS_JSH_SRC}"
+    for f in "${EXTRAS_JSH_SRC}"/*.ss; do
+        [ -f "$f" ] || continue
+        bn=$(basename "$f")
+        # Don't overwrite core modules that may already exist via jsh-src
+        if [ ! -f "${REPO_ROOT}/jsh-src/jsh/${bn}" ]; then
+            cp "$f" "${REPO_ROOT}/jsh-src/jsh/${bn}"
+        fi
+    done
+    echo "==> [3d/5] Re-translating jsh-src to include extras modules"
+    "${JERBUILD}" exec --libdirs "${RUN_LIBDIRS}" support/jerbuild.ss "${REPO_ROOT}/jsh-src" "${REPO_ROOT}/src" --force
+fi
+# Translate extras .ss modules to .sls if present
+if [ -d "${EXTRAS_JSH_SRC}" ]; then
+    echo "==> [3c/5] Translating extras jsh modules from ${EXTRAS_JSH_SRC}"
+    "${JERBUILD}" exec --libdirs "${RUN_LIBDIRS}" support/jerbuild.ss "${EXTRAS_JSH_SRC}" "${REPO_ROOT}/src" --force
+fi
+
 # ── [4/5] Regenerate jsh-generated.ss ──────────────────────────────────────
 export JSH_FEATURES="${JSH_FEATURES:-}"
 echo "==> [4/5] Regenerating jsh-generated.ss (features='${JSH_FEATURES}')"
@@ -256,4 +280,19 @@ else
     [ -n "${_FUSE_AR}" ] && "${_FUSE_AR}" rcs "${FUSE_AR_OUT}" || true
 fi
 
+# ── yubikey-native Rust archive ────────────────────────────────────────
+YUBIKEY_NATIVE_DIR="${VENDOR}/jerboa-yubikey/yubikey-native"
+YUBIKEY_NATIVE_AR_OUT="${STAGE}/libyubikey_native.a"
+if [ -f "${YUBIKEY_NATIVE_DIR}/target/release/libyubikey_native.a" ]; then
+    echo "==> Staging yubikey-native archive -> ${YUBIKEY_NATIVE_AR_OUT}"
+    cp -f "${YUBIKEY_NATIVE_DIR}/target/release/libyubikey_native.a" "${YUBIKEY_NATIVE_AR_OUT}"
+elif [ -f "${YUBIKEY_NATIVE_DIR}/target/${RUST_TARGET}/release/libyubikey_native.a" ]; then
+    echo "==> Staging yubikey-native archive -> ${YUBIKEY_NATIVE_AR_OUT}"
+    cp -f "${YUBIKEY_NATIVE_DIR}/target/${RUST_TARGET}/release/libyubikey_native.a" "${YUBIKEY_NATIVE_AR_OUT}"
+else
+    echo "==> yubikey-native not found; creating empty ${YUBIKEY_NATIVE_AR_OUT}"
+    [ -n "$(command -v ar || command -v llvm-ar || true)" ] && \
+        "$(command -v ar || command -v llvm-ar)" rcs "${YUBIKEY_NATIVE_AR_OUT}" || true
+fi
+
 echo "==> stage-for-jerbuild-linux: done"