Start

From nothing to a signed app on three systems, one a person, a script and an agent can all drive.

Who this is for
00

Who this is for

You are probably here because
  • you have a CLI, a build or a local model and it needs a face
  • 85 MB to draw a button strikes you as absurd
  • the same window has to look right on three systems
  • every control has to be reachable by name, not by pixel
  • something other than a person has to drive it
And the shape of it is
  • markup for the window
  • your own language for the logic, any of them
  • one binary, no runtime beside it
  • that is the whole shape
Get it building
01

Build it

git clone https://github.com/eddime/spark-native
cd spark-native
./spark build          # this machine
# what this machine can build, and what is missing
./spark doctor

macOS needs Xcode's command line tools. Linux needs the fontconfig headers; GTK itself is opened at run time and is not needed to build. Windows builds from either through zig cc, which spark setup fetches into .spark/.

A new app

./spark new myapp
./spark dev myapp/index.html

spark new writes a whole app that already works: a layout, a widget, a listener, a style and a script.

<window pad="24" gap="16" bg="#1a1a1a" size="480x300" title="My App">
  <style name="card" bg="#222" radius="10" pad="16">

  <text size="22" bold color="#fff">Hello</text>
  <col use="card" gap="10" self="stretch">
    <input id="who" placeholder="Your name">
    <button id="greet">Greet</button>
    <text id="out">…</text>
  </col>

  <script src="app.js"></script>
</window>
document.getElementById("greet").addEventListener("click", function () {
  document.getElementById("out").textContent =
    "Hello, " + document.getElementById("who").value;
});
Ship it
02

Ship it

spark --pack the binary document one file, 0.35 MB
./spark pack myapp/index.html      # one file, no runtime beside it
./spark build all                  # every OS this machine can reach
# .app on macOS, .desktop on Linux
./spark bundle myapp/index.html
./spark sign                       # ad-hoc, Developer ID, notarised

What the signing words actually mean

Ad-hocWhat lets a binary start at all. An arm64 binary with no signature is refused by the kernel, and lipo leaves the universal binary unsigned, so the build does this by itself.
Developer IDWhat stops the first launch elsewhere saying the developer cannot be verified. Needs a certificate in the keychain.
NotarisedApple looking at it and saying so, with the answer stapled on. One-time notarytool store-credentials, then SPARK_NOTARY_PROFILE=… spark sign.

Windows is the same shape with a .pfx, through signtool or osslsigncode. Nothing invents credentials: what is missing is named, with the command that provides it.

Keeping it up to date

version 1.2.0
macos-arm64     https://example.com/myapp-macos-arm64     <sha256>
windows-x86_64  https://example.com/myapp.exe             <sha256>
linux-x86_64    https://example.com/myapp-linux-x86_64    <sha256>
if (spark.updateCheck(url) !== VERSION && spark.updateApply(url))
  tellTheUser("restart to finish");

Plain text, because a JSON parser would be larger than the feature. Fetching is curl and hashing is shasum / sha256sum / certutil, every system ships both, so nothing is bundled here either.

Check it
03

Check it everywhere

./spark cross myapp/index.html

The same document through macOS, a Linux container and Wine. Four questions each, not one: is every widget where the layout put it, what can the keyboard reach, what does each system say every control is, and how many of the four callers reach it. Plus a screenshot from each.

The last two are what a layout check cannot see. On their first run they found a Windows field that read back its own placeholder as a value, a Linux slider that read back nothing, and a progress bar whose number had become its accessible name — with the pixels matching on all three.

Two traps this walked into so you do not have to: xvfb-run as PID 1 in a fresh container waits forever, and macOS binaries cannot be cross-built anywhere else — that is a licence, not a technical limit.

The rest of the commands

spark contract --check app.html  # what the app offers, in git
spark record app.html log.txt    # use it; every action written down
spark replay app.html log.txt    # watch it happen again
spark mcp app.html               # the window as an MCP server
spark test js                    # what the three engines answered
spark inspect app.html           # what this app may do to the machine
spark tree app.html              # the whole UI as JSON

A recording names controls rather than coordinates, so it still replays after you have moved the button. spark contract fails the build when a control is renamed, changes tag, or has its agent= tightened — one-character edits that nothing else notices and that every script written against your app notices at once.