Add repl-reader.ss: full-duplex client for the TLS debug REPL

ober

bbe0164c5b8c09070a7bb7b037b985451dc3dfb7

diff --git a/repl-reader.ss b/repl-reader.ss
new file mode 100755
index 0000000..64614a8
--- /dev/null
+++ b/repl-reader.ss
@@ -0,0 +1,98 @@
+#!/usr/bin/env jerboa
+;;; repl-reader.ss — full-duplex talk to a jcode debug REPL over TLS.
+;;;
+;;; Usage:
+;;;   ./repl-reader.ss HOST:PORT 'expr1' 'expr2' ...   ;; exprs from argv
+;;;   ./repl-reader.ss HOST:PORT                       ;; exprs from stdin
+;;;
+;;; The auth token comes from $JCODE_REPL_TOKEN, else ./.repl-token, and is
+;;; sent as the first line (the REPL's `token?` gate). TLS transport is
+;;; `openssl s_client`, because the REPL serves a per-process self-signed
+;;; cert — the token is the real auth and TLS is just wire privacy, so cert
+;;; verification is intentionally skipped (openssl's default).
+;;;
+;;; Framing: `openssl -quiet` implies -ign_eof (it never closes on our
+;;; stdin EOF), so we instead bound its lifetime with `timeout WAIT` inside
+;;; the command. We send the token + every expression, flush, and read
+;;; stdout until `timeout` kills openssl and the pipe hits EOF. WAIT seconds
+;;; (env JCODE_REPL_WAIT, default 3) is the full wall time per call — raise
+;;; it to watch a slow eval / a running sub-agent stream its reply.
+;;;
+;;; Only (chezscheme) is used: the standalone `jerboa` interpreter bundles
+;;; just a subset of (std ...), but open-process-ports is a kernel builtin.
+
+(import (chezscheme))
+
+;; ---- helpers ----
+
+(define (ws? c)
+  (or (char=? c #\space) (char=? c #\newline)
+      (char=? c #\return) (char=? c #\tab)))
+
+(define (trim s)
+  (let ((n (string-length s)))
+    (let scan-l ((i 0))
+      (cond
+        ((and (< i n) (ws? (string-ref s i))) (scan-l (+ i 1)))
+        (else
+         (let scan-r ((j n))
+           (cond
+             ((and (> j i) (ws? (string-ref s (- j 1)))) (scan-r (- j 1)))
+             (else (substring s i j)))))))))
+
+(define (read-token)
+  (let ((env (getenv "JCODE_REPL_TOKEN")))
+    (cond
+      ((and env (> (string-length env) 0)) (trim env))
+      ((file-exists? ".repl-token")
+       (trim (call-with-input-file ".repl-token" get-string-all)))
+      (else #f))))
+
+(define (die msg)
+  (display msg (current-error-port))
+  (newline (current-error-port))
+  (exit 2))
+
+;; ---- main ----
+
+(define argv (cdr (command-line-arguments)))   ;; drop the script name
+(when (null? argv)
+  (die "usage: repl-reader.ss HOST:PORT [expr ...]"))
+
+(define target (car argv))
+(define exprs  (cdr argv))
+(define token  (or (read-token)
+                   (die "no token: set JCODE_REPL_TOKEN or create .repl-token")))
+
+(define wait
+  (let ((w (getenv "JCODE_REPL_WAIT")))
+    (or (and w (string->number (trim w))) 3)))
+
+;; `exec` so the process we spawn IS timeout (clean teardown of openssl);
+;; openssl stderr (cert-verify chatter) → /dev/null; we read only stdout.
+(define cmd
+  (string-append
+    "exec timeout " (number->string wait)
+    " openssl s_client -quiet -connect " target " 2>/dev/null"))
+
+(call-with-values
+  (lambda ()
+    (open-process-ports cmd (buffer-mode block) (make-transcoder (utf-8-codec))))
+  (lambda (to-in from-out from-err pid)
+    ;; send token first, then the expressions
+    (put-string to-in token) (put-string to-in "\n")
+    (if (null? exprs)
+      (let lp ()                          ;; exprs from our stdin, one per line
+        (let ((line (get-line (current-input-port))))
+          (unless (eof-object? line)
+            (put-string to-in (trim line)) (put-string to-in "\n")
+            (lp))))
+      (for-each (lambda (e) (put-string to-in e) (put-string to-in "\n")) exprs))
+    (flush-output-port to-in)             ;; leave stdin open; `timeout` ends it
+    (let ((out (get-string-all from-out)))
+      (display out)
+      (when (and (string? out) (> (string-length out) 0)
+                 (not (char=? (string-ref out (- (string-length out) 1)) #\newline)))
+        (newline)))
+    (close-port from-out)
+    (close-port from-err)))