Distribute Jarela via portable per-OS archives and an npm package¶
Context and Problem Statement¶
To install Jarela today a user must clone the git repo and run
scripts/install-to-system.sh (macOS) or scripts/install-to-system.ps1
(Windows), both of which themselves invoke npm run build. That requires
git, Node 22+, npm, and the entire repo on disk before the app does
anything. For anyone outside the development loop this is a wall.
How should we ship Jarela so that (a) a non-developer can install it
without cloning, (b) developers who already have Node can install it the
same way they install any other CLI, (c) we don't pay for code-signing
certificates yet, and (d) the existing install-to-system scripts —
which already handle LaunchAgent / Scheduled Task wiring — remain the
single source of truth for OS integration?
Decision Drivers¶
- Two install paths cover the audience. A non-developer wants
"download a thing, double-click, accept the OS warning." A developer
wants
npm install -g jarela. Anything else is overkill. - Existing install scripts are the OS-integration layer. They
already wire LaunchAgent / Scheduled Task, split secret/non-secret
env, and handle
--skip-buildfor a pre-built tree. Re-implementing any of that inside a new installer (Tauri, electron-builder, MSIX, WiX) would duplicate logic and add a tooling dependency. - No paid certs (yet). Apple Developer ID and Authenticode certs cost money and require secret management in CI. The first release ships unsigned; users accept the OS warning once. Re-evaluate when install volume justifies the cost.
- Native deps must match the host OS.
better-sqlite3,keytar, and the Baileys deps ship platform-specific prebuilts duringnpm ci. A standalone tree built on Linux will not load on macOS. This forces either per-OS archives or per-host install. - Single tag → all artifacts. One
git tag v…push must produce every distribution channel. No human runningnpm publishfrom a laptop. - Strongly favor simplicity (global CLAUDE.md). Each layer of packaging has to justify itself.
Considered Options¶
- A. Per-OS portable archives + npm package (chosen).
GitHub Actions matrix builds on
macos-latest,windows-latest,ubuntu-latest. Each runner runsnpm run build, stages the.next/standalone/tree +.next/static/+public/+ the matching install script +package.json, and produces a.tar.gz/.zip. A separate publish job runsnpm publishfrom one of the runners. Source-only npm tarball;jarelaCLI builds on first invocation. - B. Per-OS archives only. Drop npm publish. Developers still have to clone for the dev workflow, but the GH release archives cover non-developer install. Simplest, but loses the one-line install path that npm gives free.
- C. Native installers (.dmg + create-dmg, .msi via WiX, .exe via electron-builder). Best end-user UX (drag to Applications, Add/Remove Programs entry). Requires per-installer tooling pinned in CI, and the install steps the existing scripts perform (LaunchAgent / Scheduled Task) need a second implementation inside each installer. Doubles the maintenance surface.
- D. Tauri / Electron wrapper. Treat Jarela as a desktop app. Wraps the Next.js process inside a native window, ships a single binary per OS. Heavy: introduces a Rust / native build chain, changes the runtime model from "browser → localhost" to "embedded WebView", and would need its own ADR for the runtime shift. Out of scope for "ship a download link this week."
- E. Per-OS npm sub-packages (the esbuild/swc model:
@jarela/cli-darwin-arm64+ meta package that selects the right one at install time). Solves the native-dep matching for npm, but at the cost of three publish steps per release and a dispatch package. Not justified for current scale.
Decision Outcome¶
Chosen option: A.
A is the only option that covers both audiences (non-developer + dev)
while keeping the existing install-to-system scripts as the single
OS-integration source of truth. Archives are a thin packaging step over
what the scripts already expect; npm publish is a thin packaging step
over what npm run build already produces. No new toolchain, no new
secrets needed beyond an NPM_TOKEN, no per-installer code path.
Per-OS archive layout¶
Each archive expands to jarela-<version>-<os>/ containing:
jarela-<version>-<os>/
├── .next/standalone/ # runtime tree (built on this OS)
├── .next/static/ # static assets — install script reads from here
├── public/ # public files — install script reads from here
├── scripts/
│ ├── install-to-system.sh # mac/linux bundle only
│ ├── _jarela-tailscale.sh # "
│ ├── install-to-system.ps1 # windows bundle only
│ ├── installed-launcher.ps1 # "
│ ├── installed-launcher.vbs # "
│ └── _jarela-tailscale.ps1 # "
├── package.json # version reference; install script checks for it
└── INSTALL.md # extract → run install script with --skip-build
mac / linux bundles ship the bash script set; windows bundles
ship the PowerShell set. The install scripts already accept
--skip-build / -SkipBuild, so they reuse the pre-built tree from
the matching-OS runner.
npm package shape¶
Lazy build, single platform-agnostic source tarball:
- Flip
package.jsonprivate: true→ remove. - Add
bin: { "jarela": "./scripts/jarela-bin.mjs" }. - Add a
fileswhitelist so the tarball ships source +scripts/+public/+ configs only — no.next/, nonode_modules/, no tests, no dotfiles. jarela-bin.mjs: if.next/standalone/server.jsis missing, runnpm run buildfrom the install location, then chain intostart-prod.mjs. First run takes ~30–60 s; subsequent runs are instant.- Move build-time dependencies (
tailwindcss,@tailwindcss/postcss,postcss,autoprefixer,typescript,@types/*) fromdevDependenciestodependencies. Reason: a globalnpm installdoes not pull devDependencies, but the lazy-build path needs them. Eslint stays in devDeps.
Why no native installers (.dmg / .msi)¶
Two reasons. First, the OS-integration work — autostart at login,
process supervision, env-var splitting between plist and a
mode-600 secret file — already lives in the install scripts. Wrapping
those in a .dmg would mean either calling the scripts from a
postinstall step (which gives the user nothing the archive doesn't)
or rewriting their logic in the installer's native language (double
maintenance). Second, signing-and-notarization is the actual UX win
of native installers, and we have decided to defer signing.
Why one platform-agnostic npm tarball, not three per-OS¶
Native deps (better-sqlite3, keytar) install platform-specific
prebuilds when npm pulls them on the user's machine — not from the
publish-time tarball. So a single source tarball is platform-agnostic
as long as we don't pre-bake .next/standalone/. Pre-baking would
require either three sub-packages (option E) or a postinstall rebuild,
both of which cost more than the 30–60 s first-run build.
Release trigger¶
Single workflow .github/workflows/release.yml, fires on
push of tags matching v*. Matrix produces three archives, then a
single npm publish job runs. Both gated on the tag.
Consequences¶
- Good, because
git tag v0.2.0 && git push --tagsproduces every artifact channel without further human action. - Good, because the install scripts remain the single source of truth for OS integration; archives just feed them a pre-built tree.
- Good, because npm-installed users get a CLI (
jarela) that auto-builds once and starts immediately thereafter — no separate "install then build" step to document. - Good, because no paid certs are required to ship.
- Bad, because users see an OS security warning on first launch from the unsigned archive (macOS "unidentified developer", Windows SmartScreen). Mitigated by INSTALL.md including the right-click-Open / "More info → Run anyway" workaround. Re-evaluate when install volume justifies signing.
- Bad, because
npm install -g jarelapulls more deps than necessary (build deps now independencies). Acceptable: every npm user is a build user in the lazy-build model. - Bad, because the first
jarelainvocation blocks for 30–60 s on the build. Acceptable: it happens once per install, with progress output. - Bad, because we now have three OS-specific build artifacts to keep green in CI; an arch-specific bug surfaces later than in the single-runner CI. Mitigated: the existing CI continues to gate on ubuntu-latest, and tag-push releases run all three before publishing.
Pros and Cons of the Options¶
A. Archives + npm¶
- Good, because covers both audiences with one workflow.
- Good, because reuses existing install scripts — no duplicate OS-integration logic.
- Good, because zero paid certs and zero new tooling.
- Bad, because first-run npm build delay.
B. Archives only¶
- Good, because simplest possible CI workflow.
- Bad, because no
npm install -g jarelapath; developers still have to clone.
C. Native installers¶
- Good, because best end-user "just works" experience.
- Bad, because requires per-installer tooling (
create-dmg, WiX or MSIX, possibly NSIS) pinned and maintained in CI. - Bad, because OS-integration logic gets duplicated between the installer's preinstall/postinstall hooks and the existing scripts.
- Bad, because the actual UX win — no scary warning — needs paid signing certs, which we are deferring.
D. Tauri / Electron¶
- Good, because produces a single per-OS binary that looks like a desktop app.
- Bad, because changes the runtime model (embedded WebView vs. browser → localhost) and requires its own ADR.
- Bad, because adds a Rust toolchain (Tauri) or Chromium bundle (Electron) — both heavy.
E. Per-OS npm sub-packages¶
- Good, because solves the platform-prebuilt problem for npm.
- Bad, because three publishes per release and a dispatch package to maintain. Disproportionate for current scale.
More Information¶
- Builds on [[adr-0006-windows-state-dir-localappdata]] (Windows
install layout) and [[adr-0009-in-app-http-proxy-configuration]]
(proxy env split between plist and
~/.jarela/proxy.env). - The first-launch experience for unsigned macOS archives:
xattr -dr com.apple.quarantine jarela-<version>-darwin/, documented in INSTALL.md. - Follow-ups (deferred):
- Signing — Apple Developer ID + Authenticode — once install volume justifies the cost.
arm64vsx86_64macOS matrix split. Currentmacos-latestis arm64 only; Intel Mac users would need amacos-13job.- Auto-update mechanism. Currently users re-download /
npm update -g jarela.