Register SSM copy command for AWS feature

ober

f08e0fb95818872f56b4bf65aa055841af1941d6

diff --git a/build-jsh-cross.ss b/build-jsh-cross.ss
index 27d8e0f..ce02b51 100644
--- a/build-jsh-cross.ss
+++ b/build-jsh-cross.ss
@@ -316,7 +316,7 @@
     "logs/api" "logs/operations"
     "sns/api" "sns/operations"
     "sqs/api" "sqs/operations"
-    "ssm/api" "ssm/operations" "pssm"
+    "ssm/api" "ssm/operations" "pssm" "ssmcp"
     "rds/api" "rds/db-instances"
     "elbv2/api" "elbv2/operations"
     "cfn/api" "cfn/stacks"
diff --git a/build-jsh-freebsd.ss b/build-jsh-freebsd.ss
index dc8f868..cb13fc6 100644
--- a/build-jsh-freebsd.ss
+++ b/build-jsh-freebsd.ss
@@ -387,7 +387,7 @@
         "logs/api" "logs/operations"
         "sns/api" "sns/operations"
         "sqs/api" "sqs/operations"
-        "ssm/api" "ssm/operations" "pssm"
+        "ssm/api" "ssm/operations" "pssm" "ssmcp"
         "rds/api" "rds/db-instances"
         "elbv2/api" "elbv2/operations"
         "cfn/api" "cfn/stacks"
@@ -859,6 +859,7 @@
           "jerboa-aws/sns/api" "jerboa-aws/sns/operations"
           "jerboa-aws/sqs/api" "jerboa-aws/sqs/operations"
           "jerboa-aws/ssm/api" "jerboa-aws/ssm/operations" "jerboa-aws/pssm"
+          "jerboa-aws/ssmcp"
           "jerboa-aws/rds/api" "jerboa-aws/rds/db-instances"
           "jerboa-aws/elbv2/api" "jerboa-aws/elbv2/operations"
           "jerboa-aws/cfn/api" "jerboa-aws/cfn/stacks"
diff --git a/build-jsh-macos.ss b/build-jsh-macos.ss
index 2c2c452..cad1383 100644
--- a/build-jsh-macos.ss
+++ b/build-jsh-macos.ss
@@ -558,7 +558,7 @@
         "logs/api" "logs/operations"
         "sns/api" "sns/operations"
         "sqs/api" "sqs/operations"
-        "ssm/api" "ssm/operations" "pssm"
+        "ssm/api" "ssm/operations" "pssm" "ssmcp"
         "rds/api" "rds/db-instances"
         "elbv2/api" "elbv2/operations"
         "cfn/api" "cfn/stacks"
diff --git a/build-jsh-musl.ss b/build-jsh-musl.ss
index 6f3551d..fb5ea43 100644
--- a/build-jsh-musl.ss
+++ b/build-jsh-musl.ss
@@ -331,7 +331,7 @@
         "logs/api" "logs/operations"
         "sns/api" "sns/operations"
         "sqs/api" "sqs/operations"
-        "ssm/api" "ssm/operations" "pssm"
+        "ssm/api" "ssm/operations" "pssm" "ssmcp"
         "rds/api" "rds/db-instances"
         "elbv2/api" "elbv2/operations"
         "cfn/api" "cfn/stacks"
@@ -769,6 +769,7 @@
           "jerboa-aws/sns/api" "jerboa-aws/sns/operations"
           "jerboa-aws/sqs/api" "jerboa-aws/sqs/operations"
           "jerboa-aws/ssm/api" "jerboa-aws/ssm/operations" "jerboa-aws/pssm"
+          "jerboa-aws/ssmcp"
           "jerboa-aws/rds/api" "jerboa-aws/rds/db-instances"
           "jerboa-aws/elbv2/api" "jerboa-aws/elbv2/operations"
           "jerboa-aws/cfn/api" "jerboa-aws/cfn/stacks"
diff --git a/features.def b/features.def
index d123703..96ac779 100644
--- a/features.def
+++ b/features.def
@@ -309,7 +309,7 @@
     ;; AWS — AWS CLI integration
     ;; ═══════════════════════════════════════════════════════════════════
     (aws
-      (description . "AWS CLI (,aws ,pssm)")
+      (description . "AWS CLI (,aws ,pssm ,ssmcp)")
       (modules "optional-static" "aws" "aws-static")
       (boot-so)
       (boot-so-jsh "aws")
@@ -320,7 +320,7 @@
       ;; (see src/jsh/aws.sls); force into the static closure so jerbuild
       ;; bundles them. Each entry is (<library> <one-export>) for retention.
       (bundle-libs ((jsh aws-static) aws-static-registered?))
-      (commands "aws" "pssm"))
+      (commands "aws" "pssm" "ssmcp"))
 
     ;; ═══════════════════════════════════════════════════════════════════
     ;; WORM — jerboa-wormhole text/file transfer
diff --git a/jerboa-src/src/jsh/aws-static.ss b/jerboa-src/src/jsh/aws-static.ss
index 35f67ab..551d264 100644
--- a/jerboa-src/src/jsh/aws-static.ss
+++ b/jerboa-src/src/jsh/aws-static.ss
@@ -6,9 +6,11 @@
 (import (rnrs)
         (jsh optional-static)
         (prefix (jerboa-aws cli main) %aws-cli-)
-        (prefix (jerboa-aws pssm) %aws-pssm-))
+        (prefix (jerboa-aws pssm) %aws-pssm-)
+        (prefix (jerboa-aws ssmcp) %aws-ssmcp-))
 
 (define aws-static-registered? #t)
 
 (optional-static-register! 'aws 'main %aws-cli-main)
 (optional-static-register! 'aws 'pssm-main %aws-pssm-pssm-main)
+(optional-static-register! 'aws 'ssmcp-main %aws-ssmcp-ssmcp-main)
diff --git a/jerboa-src/src/jsh/aws.ss b/jerboa-src/src/jsh/aws.ss
index 89a92be..1a4d287 100644
--- a/jerboa-src/src/jsh/aws.ss
+++ b/jerboa-src/src/jsh/aws.ss
@@ -1,8 +1,8 @@
 ;;; (jsh aws) — AWS comma commands for jsh
 ;;;
-;;; Self-registers ",aws" and ",pssm" with the meta-command registry at
+;;; Self-registers ",aws", ",pssm", and ",ssmcp" with the meta-command registry at
 ;;; module init. The actual implementations live in the jerboa-aws package
-;;; ((jerboa-aws cli main) and (jerboa-aws pssm)). When that package is not
+;;; ((jerboa-aws cli main), (jerboa-aws pssm), and (jerboa-aws ssmcp)). When that package is not
 ;;; built into the binary, the registered handlers report "not available"
 ;;; instead of crashing the shell.
 ;;;
@@ -19,6 +19,7 @@
 
 (define *aws-cli-main* #f)
 (define *pssm-main* #f)
+(define *ssmcp-main* #f)
 (define *aws-loaded?* #f)
 (define *aws-load-attempted?* #f)
 
@@ -29,11 +30,13 @@
     [else
      (set! *aws-load-attempted?* #t)
      (let ([static-main (optional-static-ref 'aws 'main)]
-           [static-pssm (optional-static-ref 'aws 'pssm-main)])
-       (if (and static-main static-pssm)
+           [static-pssm (optional-static-ref 'aws 'pssm-main)]
+           [static-ssmcp (optional-static-ref 'aws 'ssmcp-main)])
+       (if (and static-main static-pssm static-ssmcp)
          (begin
            (set! *aws-cli-main* static-main)
            (set! *pssm-main* static-pssm)
+           (set! *ssmcp-main* static-ssmcp)
            (set! *aws-loaded?* #t)
            #t)
          (guard (e [#t #f])
@@ -41,6 +44,8 @@
              (eval 'main (environment '(jerboa-aws cli main))))
            (set! *pssm-main*
              (eval 'pssm-main (environment '(jerboa-aws pssm))))
+           (set! *ssmcp-main*
+             (eval 'ssmcp-main (environment '(jerboa-aws ssmcp))))
            (set! *aws-loaded?* #t)
            #t)))]))
 
@@ -132,6 +137,16 @@
                    (tokenize-args args-string))])
        (run-with-capture 'pssm *pssm-main* args))]))
 
+(define (ssmcp-handler args-string)
+  (cond
+    [(not (aws-load!))
+     (cons "ssmcp: jerboa-aws not available in this build" 1)]
+    [else
+     (let ([args (if (string=? args-string "")
+                   '("--help")
+                   (tokenize-args args-string))])
+       (run-with-capture 'ssmcp *ssmcp-main* args))]))
+
 ;; ---- Self-registration ----
 ;; Runs at module init when jsh.ss imports (jsh aws).
 
@@ -146,3 +161,5 @@
   "AWS CLI (ec2, s3, sts, iam, ...) — provided by jerboa-aws package")
 (meta-register! "pssm" pssm-handler
   "parallel SSM across EC2 instances — provided by jerboa-aws package")
+(meta-register! "ssmcp" ssmcp-handler
+  "recursive upload over AWS SSM — provided by jerboa-aws package")
diff --git a/jsh.ss b/jsh.ss
index a65eff2..971efd0 100644
--- a/jsh.ss
+++ b/jsh.ss
@@ -5,7 +5,7 @@
         (jsh limits)
         (jsh coreutils)
         ;; Optional packages — imported for their meta-register! init side-effects.
-        ;; (jsh aws) registers ,aws and ,pssm if jerboa-aws is built in;
+        ;; (jsh aws) registers ,aws, ,pssm, and ,ssmcp if jerboa-aws is built in;
         ;; otherwise the registered handlers report "not available" gracefully.
         (jsh aws)
         ;; (jsh pass) registers ,pass and ,pass-store. Both are gated on the
@@ -253,7 +253,8 @@
         "  ,play --live node://host:port/cookie"))
     (help-lines-if 'aws
       '("  ,aws <service> <action>   AWS CLI (ec2, s3, sts, iam, ...)"
-        "  ,pssm <pattern> <cmd>     parallel SSM across EC2 instances"))
+        "  ,pssm <pattern> <cmd>     parallel SSM across EC2 instances"
+        "  ,ssmcp <pattern> SRC DST  recursive upload over AWS SSM"))
     (help-lines-if 'yubikey
       '("  ,yk                      show YubiKey presence"
         "  ,yubi                    show YubiKey presence"
@@ -746,7 +747,7 @@
           entries)
         (embed-file-set! "jsh-history" (get-output-string port))))))
 
-;; ,aws and ,pssm are registered by (jsh aws) at module init via meta-register!.
+;; ,aws, ,pssm, and ,ssmcp are registered by (jsh aws) at module init via meta-register!.
 ;; If jerboa-aws is not built into this binary, the registered handlers
 ;; gracefully report "not available" instead of crashing the shell.
 ;; Reference the sentinel so Chez actually invokes the library — without a
@@ -6419,7 +6420,7 @@
                  (else
                   (fprintf (current-error-port) ",vault: unknown subcommand '~a'~n" subcmd)
                   (cons "" 2))))))))
-        ;; ,aws and ,pssm are dispatched via the meta-command registry —
+        ;; ,aws, ,pssm, and ,ssmcp are dispatched via the meta-command registry —
         ;; (jsh aws) self-registers them at module init.
         ;; Normal Jerboa eval (with special handling for with-sandbox)
         (#t
diff --git a/test/test-features-commands.sh b/test/test-features-commands.sh
index 8068e48..3303775 100644
--- a/test/test-features-commands.sh
+++ b/test/test-features-commands.sh
@@ -102,7 +102,7 @@ while read -r feature_name cmds; do
     done
 done < "$TMPFILE"
 
-# Explicit positive assertions for aws and pssm when aws is enabled
+# Explicit positive assertions for aws, pssm, and ssmcp when aws is enabled
 echo ""
 echo "--- Explicit AWS assertions ---"
 if feature_enabled "aws"; then
@@ -120,8 +120,15 @@ if feature_enabled "aws"; then
         echo "FAIL: ,pssm --help should work (aws enabled)"
         FAILURES=$((FAILURES + 1))
     fi
+    if "$JSH" -c ',ssmcp --help' >/dev/null 2>&1; then
+        echo "PASS: ,ssmcp --help (aws enabled, explicit check)"
+        PASSES=$((PASSES + 1))
+    else
+        echo "FAIL: ,ssmcp --help should work (aws enabled)"
+        FAILURES=$((FAILURES + 1))
+    fi
 else
-    for cmd in aws pssm; do
+    for cmd in aws pssm ssmcp; do
         if output=$("$JSH" -c ",${cmd}" 2>&1); then rc=0; else rc=$?; fi
         if [ "$rc" -ne 0 ] && echo "$output" | grep -qi "not available"; then
             echo "PASS: ,$cmd reports 'not available' (aws disabled)"