jpkg: select host-specific jerboa-code package

ober

5e00c7c4261b449d3f0e1a1e06362bf3478531a9

diff --git a/docs/jpkg-guide.md b/docs/jpkg-guide.md
index 88b5b16..3e576a9 100644
--- a/docs/jpkg-guide.md
+++ b/docs/jpkg-guide.md
@@ -32,8 +32,9 @@ defaults, enforced by code with tests.
   files, setuid bits, and oversize archives are rejected up front.
 - **The lockfile is the security boundary.** `jpkg.lock` pins exact
   versions, registries, artifact digests, and manifest digests.
-  `jpkg install` materializes *exactly* the lock — it never silently
-  resolves something new.
+  Bare `jpkg install` materializes *exactly* the lock — it never silently
+  resolves something new. Supplying a package name explicitly requests
+  resolution and writes the resulting exact lock.
 - **Signed metadata, not trusted transport.** Registries use
   [TUF](https://theupdateframework.io/): threshold-signed root/targets/
   snapshot/timestamp roles with **rollback** and **freeze** protection and
@@ -81,8 +82,9 @@ the store   = ~/.jerboa/pkg — verified artifacts, shared across projects
 ```
 
 - `add` / `update` resolve ranges → write `jpkg.lock`.
-- `install` reads `jpkg.lock` and only the lock — deterministic, offline-
-  friendly, CI-safe.
+- Bare `install` reads `jpkg.lock` and only the lock — deterministic,
+  offline-friendly, CI-safe. `install PKG` is the explicit resolve-and-lock
+  convenience form.
 - Installing copies verified files; it never executes them. `build` runs
   code, in a sandbox, under policy.
 
@@ -180,7 +182,7 @@ registry, artifact-sha256, artifact-size, manifest-sha256, dependencies,
 yank status — or, for a dev link, a `link` path with no digests (flagged
 non-reproducible). Entries are sorted; the file round-trips deterministically.
 
-`jpkg install` installs **exactly** this and nothing else.
+Bare `jpkg install` installs **exactly** this and nothing else.
 
 ### 4.3 `jpkg.policy.sexp` — optional project policy override
 
@@ -209,7 +211,7 @@ Global: `jpkg --help`, `jpkg --version`. Every command also works as
 | `jpkg new NAME` | Scaffold a new package directory with manifest, strict policy, lockfile, `src/main.ss`, README, and `.build.yml`. |
 | `jpkg add PKG[@VERSION]` | Add a dependency (range), resolve, write lock, install. |
 | `jpkg remove PKG` | Drop a dependency, re-resolve, prune the environment. |
-| `jpkg install` | Install **exactly** `jpkg.lock` (no resolution). |
+| `jpkg install [PKG[@VERSION]]` | Install a package for this host, or with no argument install **exactly** `jpkg.lock`. |
 | `jpkg update [PKG ...]` | Re-resolve to newer allowed versions. |
 | `jpkg uninstall PKG` | Remove a package from the project environment only. |
 | `jpkg list` | List locked packages. |
diff --git a/lib/std/pkg/cli.ss b/lib/std/pkg/cli.ss
index 29aba43..4eeaefd 100644
--- a/lib/std/pkg/cli.ss
+++ b/lib/std/pkg/cli.ss
@@ -56,8 +56,8 @@
            "add dependency and update lockfile"            cmd-add)
      (list "remove"    "jpkg remove PKG"
            "remove dependency and update lockfile"         cmd-remove)
-     (list "install"   "jpkg install"
-           "install exactly what jpkg.lock describes"      cmd-install)
+     (list "install"   "jpkg install [PKG[@VERSION]]"
+           "install a package, or exactly replay jpkg.lock" cmd-install)
      (list "update"    "jpkg update [PKG ...]"
            "resolve newer allowed versions"                cmd-update)
      (list "uninstall" "jpkg uninstall PKG"
diff --git a/lib/std/pkg/commands.ss b/lib/std/pkg/commands.ss
index ff9f859..8f0136e 100644
--- a/lib/std/pkg/commands.ss
+++ b/lib/std/pkg/commands.ss
@@ -277,11 +277,15 @@
       0))
 
   (def (cmd-install args)
-    (unless (null? args)
-      (jpkg-error "usage: jpkg install   (installs exactly jpkg.lock)"))
-    (let ([pkgs (project-install)])
-      (say "installed ~a package~a from jpkg.lock" (length pkgs)
-           (if (= (length pkgs) 1) "" "s"))
+    (unless (or (null? args)
+                (and (pair? args) (null? (cdr args))))
+      (jpkg-error "usage: jpkg install [PKG[@VERSION]]"))
+    (let ([pkgs (if (null? args)
+                    (project-install)
+                    (project-add (car args)))])
+      (say "installed ~a package~a~a" (length pkgs)
+           (if (= (length pkgs) 1) "" "s")
+           (if (null? args) " from jpkg.lock" ""))
       (say-packages pkgs)
       0))
 
diff --git a/lib/std/pkg/project.ss b/lib/std/pkg/project.ss
index d0a6a5b..a84cb9f 100644
--- a/lib/std/pkg/project.ss
+++ b/lib/std/pkg/project.ss
@@ -394,6 +394,23 @@
                (char=? (string-ref env 0) #\@))
           env
           (string-append "@" (or env "ober")))))
+  (def (host-package-name name)
+    ;; Logical packages may publish host-specific binary variants. Keep the
+    ;; physical tuple in the lockfile so subsequent installs remain exact.
+    (if (string=? name "@ober/jerboa-code")
+        (string-append
+         name
+         (case (machine-type)
+           [(a6le ta6le) "-linux-amd64"]
+           [(arm64le tarm64le) "-linux-arm64"]
+           [(a6fb ta6fb) "-freebsd-amd64"]
+           [(arm64fb tarm64fb) "-freebsd-arm64"]
+           [(a6osx ta6osx) "-macos-amd64"]
+           [(arm64osx tarm64osx) "-macos-arm64"]
+           [else
+            (jpkg-error "no jerboa-code artifact for host machine ~a"
+                        (machine-type))]))
+        name))
 
   (def (parse-pkg-spec spec)
     ;; "PKG" or "PKG@VERSION-OR-RANGE" -> (values name range-or-#f)
@@ -407,9 +424,10 @@
                (string-append (default-scope) "/" spec))])
       (let loop ([i 1])   ;; skip the leading @ of the scope
         (cond
-          [(>= i (string-length spec)) (values spec #f)]
+          [(>= i (string-length spec))
+           (values (host-package-name spec) #f)]
           [(char=? (string-ref spec i) #\@)
-           (values (substring spec 0 i)
+           (values (host-package-name (substring spec 0 i))
                    (substring spec (+ i 1) (string-length spec)))]
           [else (loop (+ i 1))]))))
 
diff --git a/tests/test-jpkg-project.ss b/tests/test-jpkg-project.ss
index 1d57bca..e339092 100644
--- a/tests/test-jpkg-project.ss
+++ b/tests/test-jpkg-project.ss
@@ -42,6 +42,17 @@
 (define world (format "/tmp/jpkg-proj-~a" (random-suffix)))
 (define reg1 (path-concat world "registry1"))
 (define reg2 (path-concat world "registry2"))
+(define host-jcode-package
+  (string-append
+   "@ober/jerboa-code"
+   (case (machine-type)
+     [(a6le ta6le) "-linux-amd64"]
+     [(arm64le tarm64le) "-linux-arm64"]
+     [(a6fb ta6fb) "-freebsd-amd64"]
+     [(arm64fb tarm64fb) "-freebsd-arm64"]
+     [(a6osx ta6osx) "-macos-amd64"]
+     [(arm64osx tarm64osx) "-macos-arm64"]
+     [else (error 'test-jpkg-project "unsupported test host" (machine-type))])))
 (define orig-dir (current-directory))
 
 (mkdir-p world)
@@ -74,6 +85,7 @@
 (publish! reg1 "@lib/base" "1.0.0" '())
 (publish! reg1 "@lib/base" "1.5.0" '())
 (publish! reg1 "@lib/extra" "1.0.0" '(("@lib/base" . "^1.0.0")))
+(publish! reg1 host-jcode-package "0.1.1" '())
 
 ;; ── project flow ────────────────────────────────────────────────────────
 
@@ -198,6 +210,15 @@
               (let ([r2 (run-jpkg '("install"))])
                 (and (= (car r2) 0)
                      (not (file-exists? ".jpkg/deps/lib-extra" #f)))))))
+(check "install-selects-host-package"
+       (let ([r (run-jpkg '("install" "jerboa-code@0.1.1"))])
+         (and (= (car r) 0)
+              (s-contains? (cadr r) host-jcode-package)
+              (equal? (map locked-package-name (lock-parse-file "jpkg.lock"))
+                      (list host-jcode-package))
+              (file-directory?
+               (path-concat ".jpkg/deps" (env-dir-name host-jcode-package)))
+              (= 0 (car (run-jpkg '("remove" "jerboa-code")))))))
 
 ;; ── dependency-confusion guard ──────────────────────────────────────────
 ;; @lib/base gets locked from "main"; a higher-priority registry "evil"