updates

ober

804643109749c9cbbadab9aac8f4456221ccf9b8

diff --git a/Makefile b/Makefile
index 6de0340..b62731a 100644
--- a/Makefile
+++ b/Makefile
@@ -539,22 +539,40 @@ 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: 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
+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)
 	@features="$(JSH_CROSS_FEATURES)"; \
-	    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
+	    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"; \
+	    else \
+	        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
 	@echo "=== jsh-linux-amd64 ready ==="
 	@ls -lh jsh-linux-amd64
 	@file jsh-linux-amd64
 
-linux-arm64: 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
+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)
 	@features="$(JSH_CROSS_FEATURES)"; \
-	    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
+	    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"; \
+	    else \
+	        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
 	@echo "=== jsh-linux-arm64 ready ==="
 	@ls -lh jsh-linux-arm64
 	@file jsh-linux-arm64
diff --git a/main.ss b/main.ss
index 93838d8..e8f8071 100644
--- a/main.ss
+++ b/main.ss
@@ -310,6 +310,9 @@
                                                     (substring prompt-str nl (string-length prompt-str)))
                                      (string-append prompt-str sfx)))))]
              ;; Create completion function
+             [ps2-str (expand-prompt (or (env-get env "PS2") "> ")
+                                     (lambda (name) (env-get env name))
+                                     0 0 0 #f 0)]
              [complete-fn (lambda (line cursor)
                            (complete-word line cursor env))]
              ;; Read input with line editor
@@ -350,31 +353,44 @@
                  ;; Show expanded line if it changed
                  (when (not (string=? expanded input))
                    (displayln expanded (current-error-port)))
-                 ;; Add to history when enabled.
-                 (when (*jsh-history-enabled*)
-                   (history-add! expanded))
                  ;; Parse and execute (unless :p modifier was used)
                  (when execute?
-                   (let ([status (with-catch
-                                  (lambda (e)
-                                    (cond
-                                      ((break-exception? e) 0)
-                                      ((continue-exception? e) 0)
-                                      ((return-exception? e) (return-exception-status e))
-                                      (else (raise e))))
-                                  (lambda () (execute-input expanded env)))])
-                     (let ([hook (*command-trace-hook*)])
-                       (when hook
-                         (with-catch (lambda (e) #!void)
-                                     (lambda () (hook expanded status env)))))
-                     (env-set-last-status! env status)
-                     ;; Process pending signals and execute trap commands
-                     (process-traps! env)
-                     ;; ERR trap: execute if last command failed
-                     (when (and (not (= status 0)) (trap-get "ERR"))
-                       (let ([action (trap-get "ERR")])
-                         (when (string? action)
-                           (execute-input action env)))))))))
+                   (let loop ([buffer expanded] [history-added? #f])
+                     (let ([status (with-catch
+                                    (lambda (e)
+                                      (cond
+                                        ((break-exception? e) 0)
+                                        ((continue-exception? e) 0)
+                                        ((return-exception? e) (return-exception-status e))
+                                        (else (raise e))))
+                                    (lambda () (execute-input buffer env #t)))])
+                       (cond
+                         ((eq? status 'need-more)
+                          (let ([more (line-edit ps2-str complete-fn edit-mode)])
+                            (if (eq? more 'eof)
+                              (begin
+                                (fprintf (current-error-port) "jsh: syntax error: unexpected end of file~n")
+                                (unless history-added?
+                                  (when (*jsh-history-enabled*)
+                                    (history-add! buffer)))
+                                (env-set-last-status! env 2))
+                              (loop (string-append buffer "\n" more) history-added?))))
+                         (else
+                          (unless history-added?
+                            (when (*jsh-history-enabled*)
+                              (history-add! buffer)))
+                          (let ([hook (*command-trace-hook*)])
+                            (when hook
+                              (with-catch (lambda (e) #!void)
+                                          (lambda () (hook buffer status env)))))
+                          (env-set-last-status! env status)
+                          ;; Process pending signals and execute trap commands
+                          (process-traps! env)
+                          ;; ERR trap: execute if last command failed
+                          (when (and (not (= status 0)) (trap-get "ERR"))
+                            (let ([action (trap-get "ERR")])
+                              (when (string? action)
+                                (execute-input action env))))))))))))
            ;; Restore terminal to sane state after command
            ;; (programs like top/vim may leave echo off or raw mode on).
            ;; Must use Gambit's tty-mode-set! to keep port state in sync;
@@ -578,7 +594,7 @@
   (let ([expr (string-trim-whitespace (substring input 1 (string-length input)))])
     (execute-meta-command expr)))
 
-(def (execute-input input env)
+(def (execute-input input env (allow-incomplete? #f))
   ;; Parse and execute a line of input
   (with-catch
    (lambda (e)
@@ -614,8 +630,11 @@
          ((eq? cmd 'error) 2)
          ((eq? cmd 'need-more)
           ;; Incomplete command (e.g., "while :" without "do")
-          (fprintf (current-error-port) "jsh: syntax error: unexpected end of file~n")
-          2)
+          (if allow-incomplete?
+            'need-more
+            (begin
+              (fprintf (current-error-port) "jsh: syntax error: unexpected end of file~n")
+              2)))
          ((not cmd) 0)
          (else (execute-command cmd env))))))))
 
diff --git a/test/test-binary.sh b/test/test-binary.sh
index 0aebdfb..ba23124 100755
--- a/test/test-binary.sh
+++ b/test/test-binary.sh
@@ -82,6 +82,68 @@ else
 fi
 rm -rf "$use_tmp"
 
+# Interactive REPL continuation tests require a pseudo-tty; skip if python3 is unavailable.
+if command -v python3 >/dev/null 2>&1; then
+    if python3 - "$BINARY" <<'PY'
+import pty, os, sys, select, re
+
+binary = sys.argv[1]
+master, slave = pty.openpty()
+pid = os.fork()
+if pid == 0:
+    os.close(master)
+    os.setsid()
+    os.dup2(slave, 0)
+    os.dup2(slave, 1)
+    os.dup2(slave, 2)
+    os.close(slave)
+    os.execv(binary, [binary])
+    sys.exit(1)
+os.close(slave)
+
+def read_all(timeout=1.0):
+    out = b''
+    while True:
+        r, _, _ = select.select([master], [], [], timeout)
+        if not r:
+            break
+        try:
+            data = os.read(master, 4096)
+            if not data:
+                break
+            out += data
+        except OSError:
+            break
+    return out
+
+def send(cmd):
+    os.write(master, (cmd + '\n').encode())
+    return read_all(0.5)
+
+read_all(1.0)
+send('for x in a b; do')
+send('echo "x=$x"')
+out = send('done')
+text = re.sub(r'\x1b\[[0-9;?]*[a-zA-Z]', '', out.decode('utf-8', errors='replace'))
+os.write(master, b'exit\n')
+read_all(0.5)
+os.waitpid(pid, 0)
+if 'x=a' in text and 'x=b' in text:
+    sys.exit(0)
+else:
+    print(text, file=sys.stderr)
+    sys.exit(1)
+PY
+    then
+        pass=$((pass + 1))
+    else
+        fail=$((fail + 1))
+        printf 'FAIL: interactive REPL multi-line continuation (for loop)\n'
+    fi
+else
+    echo "SKIP: interactive REPL tests require python3"
+fi
+
 rm -f /tmp/jsh-test.out /tmp/jsh-test.err
 echo "Binary tests ($BINARY): $pass passed, $fail failed"
 [ "$fail" -eq 0 ]