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
- 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
- markup for the window
- your own language for the logic, any of them
- one binary, no runtime beside it
- that is the whole shape
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
./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-hoc | What 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 ID | What stops the first launch elsewhere saying the developer cannot be verified. Needs a certificate in the keychain. |
| Notarised | Apple 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 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.