Skip to content

feat(cli/launcher): build integration + cli.wasm discovery (14a.5c)#30

Merged
hyperpolymath merged 1 commit into
feat/cli-launcher-bridges-14a5bfrom
feat/cli-launcher-build-integration-14a5c
May 20, 2026
Merged

feat(cli/launcher): build integration + cli.wasm discovery (14a.5c)#30
hyperpolymath merged 1 commit into
feat/cli-launcher-bridges-14a5bfrom
feat/cli-launcher-build-integration-14a5c

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Stacked PR. Base is `feat/cli-launcher-bridges-14a5b` (14a.5b, PR #29). The diff below shows only the 14a.5c delta. When #29 merges, this PR's base auto-rebases to main.

Summary

Phase 14a.5c — closes the prep arc. `zig build install` now produces both the launcher binary AND a compiled `cli.wasm`, placed under the install prefix's `share/gossamer/`. The launcher discovers `cli.wasm` at runtime via a standard search order.

What lands

File Change Purpose
`cli/src/Main.eph` (new) ~25 LOC Placeholder Ephapax entry point; compiles cleanly to a 715-byte .wasm
`cli/launcher/build.zig` +30 LOC `ephapax compile` build step; installs cli.wasm to `share/gossamer/`
`cli/launcher/src/main.zig` +50 LOC `findCliWasm()` discovery; argv[1] still overrides
`cli/launcher/README.adoc` +57 LOC Documents build workflow + search order

Discovery search order

  1. `$GOSSAMER_WASM` env var (explicit override).
  2. `<exe_dir>/../share/gossamer/cli.wasm` (install-prefix-relative).
  3. `/usr/local/share/gossamer/cli.wasm`
  4. `/usr/share/gossamer/cli.wasm`

Explicit `gossamer-launcher /path/to/foo.wasm` still works (MVP smoke-test workflow); `argv[1]` is treated as an override when it ends in `.wasm`.

Build workflow

```sh

Use a dev build of ephapax instead of one on PATH:

export EPHAPAX=$HOME/dev/repos/ephapax/target/debug/ephapax

cd cli/launcher
zig build install --prefix /usr/local

→ /usr/local/bin/gossamer-launcher

→ /usr/local/share/gossamer/cli.wasm

gossamer-launcher # discovers + runs cli.wasm
gossamer-launcher dev # passes 'dev' to the guest
zig build run -- dev # same via build step
```

Test plan

  • `zig ast-check` on `main.zig` + `build.zig` — clean.
  • `ephapax compile cli/src/Main.eph -o /tmp/cli.wasm` → 715-byte module.
  • Wasm imports inspected: `env::print_i32`, `env::print_string`, `env::argv_count` all present.
  • Wasm export `main` (func 14) present.
  • Reviewer: `EPHAPAX=... zig build install --prefix /usr/local` produces both `gossamer-launcher` and `cli.wasm` in the right places.
  • Reviewer: `gossamer-launcher` with no args discovers and runs the installed cli.wasm, prints `0` (argv_count when no args passed).
  • Reviewer: `gossamer-launcher one two three` prints `3`.

NOT in scope

What this concludes

This is the final Phase 14a PR. Six prep PRs across two repos:

Phase PR
Backlog: test port + linguist gossamer #25
14a.1 — `stdlib/Argv.eph` ephapax #86
14a.2 — `file_watcher.zig` relocate gossamer #24
14a.3 — shell + fs FFI exports gossamer #26
14a.4 — `gossamer_conf_load` JSON gossamer #27
14a.5a — MVP wasmtime launcher gossamer #28
14a.5b — libgossamer bridges + caps gossamer #29
14a.5c — build integration this PR

With these merged, #15 (the actual port) has a known-working launcher + bridge surface to write `Main.eph` against.

🤖 Generated with Claude Code

Phase 14a.5c. Closes the prep arc: `zig build install` now produces both
the launcher binary AND a compiled cli.wasm, placed under the install
prefix's share/gossamer/. The launcher discovers cli.wasm at runtime via
a standard search order.

New file cli/src/Main.eph (~25 lines):

  Placeholder entry point. Declares env::print_i32 and env::argv_count
  as extern "env" fns and defines `fn main(): Unit = print_i32(argv_count())`.
  Compiles cleanly via ephapax compile to a 715-byte .wasm with the
  expected `main` export. Real CLI subcommand dispatch lands in #15.

cli/launcher/build.zig (+30 LOC):

  • Reads $EPHAPAX env var (defaults to `ephapax` on PATH) so dev
    workflows can point at a sibling repo's debug build.
  • addSystemCommand wraps `ephapax compile cli/src/Main.eph -o cli.wasm`.
    addOutputFileArg makes the .wasm a tracked build artefact.
  • addInstallFileWithDir places it at `share/gossamer/cli.wasm` under
    the install prefix.
  • Run step sets GOSSAMER_WASM to the install location so `zig build
    run -- args...` uses the freshly-built wasm.

cli/launcher/src/main.zig (+50 LOC):

  • New findCliWasm(allocator) checks (in order): $GOSSAMER_WASM env
    var, <exe_dir>/../share/gossamer/cli.wasm, /usr/local/share/gossamer/
    cli.wasm, /usr/share/gossamer/cli.wasm. Returns null if none exist.
  • main() falls through to discovery when no .wasm argument is given
    (argv[1] must end in .wasm to be treated as an explicit override).
    The guest_argv slice shifts by 1 or 2 depending on whether a wasm
    path was passed, so the guest sees exactly the user's intended
    arguments regardless of invocation shape.
  • Usage message updated to document the search order.

cli/launcher/README.adoc:

  • Documents the new build workflow (EPHAPAX env var, zig build install
    --prefix, post-install discovery).
  • Documents the 4-step search order.
  • Notes that the launcher is still installed as `gossamer-launcher`
    alongside the existing `gossamer` native binary — the rename + IPC
    handler migration is #15's job.

What's NOT in this PR (intentionally):

  • Renaming gossamer-launcher → gossamer. Requires migrating the 27
    gossamer_channel_bind IPC handlers out of cli/src/main.zig first.
  • Filling in Main.eph with real CLI subcommand dispatch (dev / build /
    bundle / run / init / info). That's #15.

Verified:

  • zig ast-check on main.zig + build.zig — clean.
  • ephapax compile cli/src/Main.eph → 715-byte .wasm with correct
    imports (env::print_i32, env::argv_count, baseline ephapax) and
    `main` export.
  • Wasm module inspection confirms imports + exports match the
    launcher's expectations.

Stacked on 14a.5b (gossamer #29). When 14a.5b merges, this PR's base
auto-rebases to main.

This concludes Phase 14a (the prep arc). #15 — the actual port of
cli/src/main.zig logic into Main.eph — can now proceed against a known-
working launcher + bridge surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath merged commit 7bbbbbd into feat/cli-launcher-bridges-14a5b May 20, 2026
13 of 14 checks passed
hyperpolymath added a commit that referenced this pull request May 20, 2026
)

* feat(cli/launcher): libgossamer bridges + eager-grant caps (14a.5b)

Phase 14a.5b. Adds 29 env::gossamer_* host imports and 1 env::cap_token
to the wasmtime launcher MVP from 14a.5a. The guest can now talk to
libgossamer (open windows, navigate, run watcher, spawn shells, read
conf, etc.) through these bridges.

New file cli/launcher/src/bridges.zig (~470 LOC):

  • extern fn declarations for all 29 libgossamer symbols (matching
    src/interface/ffi/src/*.zig exports).
  • marshalling helpers: dupeZGuestString (wasm ptr/len → host
    null-terminated dupe) and writeCStringToGuest (host C string →
    guest buffer, WASI-style write-into-caller-buffer pattern).
  • One bridge callback per symbol. String inputs unmarshal via
    dupeZGuestString; string outputs use writeCStringToGuest. Opaque
    handles (watcher, shell child, conf) cross the wasm boundary as
    i64 (bit-cast u64 = host pointer).
  • A static Imports table registered with the wasmtime linker.
  • grantCaps(host_env) eager-grants tokens for ResourceKind 0
    (FileSystem), 1 (Network), 2 (Shell). The guest reads them via
    env::cap_token(kind).

Updates cli/launcher/src/main.zig:

  • HostEnv gains cap_tokens: [6]u64 keyed by ResourceKind.
  • HostEnv, ImportSpec, guestSlice, and the @cImport bound `c`
    are now `pub` so bridges.zig can use them.
  • main() calls bridges.grantCaps() and registers bridges.Imports
    alongside the 5 baseline imports.

Updates cli/launcher/build.zig to link libgossamer (added
addLibraryPath + addRPath pointing at ../../src/interface/ffi/zig-out
and linkSystemLibrary("gossamer")).

Added cli/launcher/src/hello-bridges.wat — smoke test calling
gossamer_version_to into a guest buffer and printing via print_string.
Validated with wasm-tools parse (208 bytes).

Coverage:

  String-out      gossamer_version_to / build_info_to / last_error_to
  Webview         create_ex / navigate / load_html / channel_open /
                  set_csp / set_title / registry_add / run
  Groove          groove_discover / groove_status
  Watcher         watcher_start / watcher_stop
  Shell           shell_spawn / shell_kill
  Filesystem      fs_read_text / fs_write_text / fs_exists / fs_mkdir_p /
                  fs_copy_file
  Conf            conf_load / get_string / get_int / get_bool / has / free
  Caps            cap_token

Explicitly out of scope:

  • The 27 gossamer_channel_bind IPC handlers (window_minimize, group_*,
    transmute, etc. — currently in cli/src/main.zig) DO NOT bridge
    through wasm. They live native-side. A follow-up moves them into
    libgossamer's channel_open default-handler set.
  • Building cli.wasm from .eph source — 14a.5c.
  • cli.wasm discovery at /usr/share/gossamer — 14a.5c.

Verified:

  • zig ast-check on main.zig, bridges.zig, build.zig — clean.
  • wasm-tools parse on hello-bridges.wat — 208-byte valid module.
  • Full link requires libwasmtime (14a.5a dep) AND libgossamer built
    in ../../src/interface/ffi/zig-out; both deferred to local /
    CI build environment.

Builds on top of 14a.5a (gossamer #28); PR base set accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli/launcher): build integration + cli.wasm discovery (14a.5c) (#30)

Phase 14a.5c. Closes the prep arc: `zig build install` now produces both
the launcher binary AND a compiled cli.wasm, placed under the install
prefix's share/gossamer/. The launcher discovers cli.wasm at runtime via
a standard search order.

New file cli/src/Main.eph (~25 lines):

  Placeholder entry point. Declares env::print_i32 and env::argv_count
  as extern "env" fns and defines `fn main(): Unit = print_i32(argv_count())`.
  Compiles cleanly via ephapax compile to a 715-byte .wasm with the
  expected `main` export. Real CLI subcommand dispatch lands in #15.

cli/launcher/build.zig (+30 LOC):

  • Reads $EPHAPAX env var (defaults to `ephapax` on PATH) so dev
    workflows can point at a sibling repo's debug build.
  • addSystemCommand wraps `ephapax compile cli/src/Main.eph -o cli.wasm`.
    addOutputFileArg makes the .wasm a tracked build artefact.
  • addInstallFileWithDir places it at `share/gossamer/cli.wasm` under
    the install prefix.
  • Run step sets GOSSAMER_WASM to the install location so `zig build
    run -- args...` uses the freshly-built wasm.

cli/launcher/src/main.zig (+50 LOC):

  • New findCliWasm(allocator) checks (in order): $GOSSAMER_WASM env
    var, <exe_dir>/../share/gossamer/cli.wasm, /usr/local/share/gossamer/
    cli.wasm, /usr/share/gossamer/cli.wasm. Returns null if none exist.
  • main() falls through to discovery when no .wasm argument is given
    (argv[1] must end in .wasm to be treated as an explicit override).
    The guest_argv slice shifts by 1 or 2 depending on whether a wasm
    path was passed, so the guest sees exactly the user's intended
    arguments regardless of invocation shape.
  • Usage message updated to document the search order.

cli/launcher/README.adoc:

  • Documents the new build workflow (EPHAPAX env var, zig build install
    --prefix, post-install discovery).
  • Documents the 4-step search order.
  • Notes that the launcher is still installed as `gossamer-launcher`
    alongside the existing `gossamer` native binary — the rename + IPC
    handler migration is #15's job.

What's NOT in this PR (intentionally):

  • Renaming gossamer-launcher → gossamer. Requires migrating the 27
    gossamer_channel_bind IPC handlers out of cli/src/main.zig first.
  • Filling in Main.eph with real CLI subcommand dispatch (dev / build /
    bundle / run / init / info). That's #15.

Verified:

  • zig ast-check on main.zig + build.zig — clean.
  • ephapax compile cli/src/Main.eph → 715-byte .wasm with correct
    imports (env::print_i32, env::argv_count, baseline ephapax) and
    `main` export.
  • Wasm module inspection confirms imports + exports match the
    launcher's expectations.

Stacked on 14a.5b (gossamer #29). When 14a.5b merges, this PR's base
auto-rebases to main.

This concludes Phase 14a (the prep arc). #15 — the actual port of
cli/src/main.zig logic into Main.eph — can now proceed against a known-
working launcher + bridge surface.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 20, 2026
…#31)

Moves the 28 channel-bind handlers (27 window/group/transmute/debug/
groove + 1 shell-exec) from cli/src/main.zig into the new
src/interface/ffi/src/ipc_handlers.zig. libgossamer now registers them
automatically via gossamer_channel_register_defaults; the native CLI
calls that single function instead of binding each handler manually.

Why now:

  The upcoming Ephapax-wasm CLI (#15 proper) opens a channel and would
  otherwise have to bridge 28 wasmtime_func_callback_t shims back into
  libgossamer just to do work that's already living inside libgossamer.
  Moving the defaults out of the application code makes wasm and native
  dispatch byte-identical and shrinks the cli/ surface area considerably.

Numbers:

  cli/src/main.zig         1335 → 690 lines   (-645)
  libgossamer (new file)      0 → 644 lines   (+644 ipc_handlers.zig)
  libgossamer main.zig      +10 (comptime import block)

  Net deletion in cli/, net move into libgossamer. The handler bodies
  themselves are unchanged byte-for-byte aside from the now-extern
  declarations of the gossamer_* symbols they call (since they sit one
  module away from main.zig in the libgossamer build now).

What the migration touches:

  • src/interface/ffi/src/ipc_handlers.zig (new) — extracts the whole
    IPC block from cli/src/main.zig: shellExecHandler, extractSimpleJsonField,
    the 27 window-control handlers, the bindWindowControlHandlers helper,
    and one new public export:

      pub export fn gossamer_channel_register_defaults(channel, handle_ptr) void

  • src/interface/ffi/src/main.zig — adds the comptime import line so
    the new module's exports land in libgossamer's symbol table.

  • cli/src/main.zig:
      - Removes the 27 window/group/transmute/debug/groove handlers
      - Removes shellExecHandler + extractSimpleJsonField
      - Removes bindWindowControlHandlers
      - Removes ~30 now-unused extern fn declarations (gossamer_show,
        hide, minimize, maximize, restore, resize, request_close,
        set_title, eval, emit, destroy, guard_set/get, group_*,
        raise/lower, broadcast, send_to, arrange, transmute,
        transmute_get, activity_set/get, debug_*, groove_dock/undock/
        connect_typed/disconnect_typed/query_type, channel_bind,
        channel_bind_async)
      - Adds the new extern fn gossamer_channel_register_defaults
      - Replaces two call sites in cmdDev / cmdRun:
          bindWindowControlHandlers(channel, handle) +
            gossamer_channel_bind_async(channel, "opsm_runtime", ...)
        becomes simply:
          gossamer_channel_register_defaults(channel, handle)

Verified:

  • zig ast-check on all three touched files — clean.
  • Full link requires GTK3/WebKitGTK dev headers (pre-existing baseline
    limitation in this WSL env); CI runs the full build.

First step of #15 (the gossamer CLI port to typed-wasm Ephapax). Logically
independent of the launcher prep stack (PRs #28/#29/#30) — base is main.
After this lands, the Ephapax-side Main.eph can drive the webview through
the launcher's existing bridges without re-implementing any IPC handlers.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant