Skip to content

feat: build ScanRuler per platform as an agent executable, a tray app and installers - #66

Open
Mearman wants to merge 25 commits into
CNCKitchen:mainfrom
Mearman:feat/installers
Open

Mearman wants to merge 25 commits into
CNCKitchen:mainfrom
Mearman:feat/installers

Conversation

@Mearman

@Mearman Mearman commented Nov 1, 2026

Copy link
Copy Markdown

Adds per-platform builds of ScanRuler for Linux, macOS and Windows on x64 and arm64: the agent server as a single executable, the web app with a background server, a tray desktop app, and native installers for it. CI builds, installs and starts all of them on every platform on each pull request, and a release attaches them to the tag.

It is four branches stacked, each a working step, so you can take less than all of it. This pull request is the top; the others are the same work cut shorter:

  • feat/agent-bundle: the MCP server and the headless engine as one executable per platform, with the CI matrix and release assets.
  • feat/app-bundle: adds the built app and a detached background server (scanruler-agent start|open|status|stop). An agent's stdio copy relays through it to the open tab.
  • feat/electron-shell: a tray app on Electron that attaches to or starts that server, so closing the window leaves everything running.
  • feat/installers (this pull request): a .dmg, a Windows Setup.exe (Inno Setup) and a .deb, each built with the platform's own tool and installed for real in CI.

Things for you to decide:

  • Electron ships Chromium's FFmpeg as a separate LGPL-2.1 library, and CONTRIBUTING.md allows LGPL only for Open CASCADE. One commit ("record the Electron FFmpeg library in the licence policy") adds a paragraph treating it the same way, separate and replaceable, loaded and not linked. Dropping that commit and the Electron branches leaves feat/app-bundle standing on its own.
  • The macOS apps and executables are signed ad hoc, not notarised, so a downloaded copy needs xattr -d com.apple.quarantine once. The Windows executables and setup program are unsigned.
  • New dev dependencies, with their licences: esbuild (MIT), postject (MIT), electron (MIT), @electron/packager (BSD-2-Clause) and @resvg/resvg-js (MPL-2.0). All are on the licence allowlist and the check passes. The desktop archives and installers also carry Electron's own bundled Chromium notices (LICENSES.chromium.html). The Inno Setup script, the postject type declaration and the scripts are original; no code was copied from another project. I did not use electron-builder, because its bundled 7-Zip is LGPL while the package says MIT, which the check cannot see.
  • The new scripts are TypeScript run with vite-node, checked by their own tsconfig.node.json, because the existing scripts/edge-lab files do not pass a strict check. mcp/ stays JavaScript.

I ran every branch on a separate repository on all six GitHub runners (CI, the licence check and a tag release that attached every archive and installer), the installer again on the macOS 26 runners, and the full unit and MCP suites, the bundle and the smoke tests on Linux arm64. The macOS disk image is built through a sparse image with diskutil image, because hdiutil create is deprecated and diskutil image create from rejects a folder on macOS 15.

Drafted with Claude Code.

…lined

--standalone inlines every package and copies the CAD kernel's WebAssembly to assets/ beside
the module, so the engine runs from any directory without node_modules.
…one entry

Importing the JSON lets a bundler inline it, where reading it relative to import.meta.url
cannot work once the server is bundled. The standalone entry skips the npm updater and defaults
--engine to the engine/ directory beside the executable.
Bundles the server and the standalone engine into a Node single executable, ad-hoc signed on
macOS, and a smoke test that starts it as an MCP client does and has it add a parameter.
Adds esbuild and postject as dev dependencies.

The scripts are TypeScript, run with vite-node as the repository's other TypeScript scripts are,
and type-checked by tsconfig.node.json (npm run typecheck:node).
A reusable workflow builds and smoke-tests the executable on Linux, macOS and Windows runners,
x64 and arm64. CI runs it on pull requests; the release workflow attaches the archives to the
tag's release.
…caveat

Covers what npm run bundle:agent makes, the smoke test, where CI and the release workflow run
them, and why a downloaded macOS copy needs its quarantine attribute cleared.
--daemon holds the port, serves --serve <dir> and relays to the page with no agent attached,
recording itself in the config directory. A second one finds the port held and ends. The new
daemon module starts it detached, finds it again, reports status and stops it.
start, open, status and stop commands; run from a terminal with no arguments it starts the
server and opens the app. An agent's stdio copy relays through a running server and otherwise
runs headless from the engine beside the executable.
The bundle gains app/, the built web app. The smoke test runs the executable as an agent does
and as a person does: start, fetch the app, status, a relaying agent, stop, in a config
directory and on a port of its own.
The bundle carries app/, and the executable's commands run a detached server that serves it.
…cies

electron (MIT), @electron/packager (BSD-2-Clause) and @resvg/resvg-js (MPL-2.0) are on the licence
allowlist. The renderer draws the tray and application icons from the mark's SVG, where sharp would
bring an LGPL image library.
Moves the esbuild step into scripts/lib so the executable and the desktop app bundle the server the same way.
leading arguments before --daemon and an environment let an app that runs scripts, such as
Electron running as Node, start the detached server.
One instance per user, a tray with Open and Quit, one window on the paired app, closing the window
leaves everything running, navigation kept on the app, and a smoke run that loads the page and
exits 0 or 1. Electron's API is handed in so the behaviour is tested against a fake.
The desktop shell needs the port and not the whole server.
Attaches to the background server or starts it detached, running the bundled server as Node
through the Electron binary, so it outlives the app.
npm run package:electron builds Electron with the shell, the built app, the bundled server and the
tray icon, signed ad hoc on macOS. npm run smoke:electron runs it with --smoke: the app must start
the background server, load the page into a hidden window and exit 0, and the server must still
run, serve the app and stop on request after the app has gone.
The reusable workflow becomes bundles.yml: each runner builds and smoke-tests the agent executable and
the desktop app, under a virtual display on Linux, and uploads one archive of each. Pull requests run
it through CI and a release attaches the archives to the tag.
What npm run package:electron makes, how the tray app and the background server relate, and where CI runs it.
Electron ships Chromium's FFmpeg as a separate LGPL library, loaded and not linked, as Open CASCADE is.
Separate from the code so it can be dropped on its own; it needs the maintainer's decision.
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA. Thank you.
Posted by the CLA Assistant Lite bot.

@Mearman

Mearman commented Nov 1, 2026

Copy link
Copy Markdown
Author

I agree to CLA.md

opoeta added a commit to opoeta/ScanRuler that referenced this pull request Nov 1, 2026
electron-builder bundles 7-Zip (LGPL), which CONTRIBUTING.md's dependency
policy does not allow and the licence check cannot see — the same reason
PR CNCKitchen#66 avoids it. scripts/package.mjs stages the shell, copies the web
build, the agent server and the bridge into resources/, and puts
scanruler-mcp.cmd beside the exe, in the same release/win-unpacked layout.
The electron-builder file associations went with it (they only applied
through an installer).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@opoeta

opoeta commented Nov 1, 2026

Copy link
Copy Markdown

Nice work — this is the distribution side of what #67 does on the feature side (graphics backend choice, run-time translation, Creality Scan projects, agent on the open window), and the two were opened an hour apart without knowing of each other. I have moved #67 to @electron/packager for the same 7-Zip reason you give, and its features are in separate modules, so if yours is the shell that lands I would rebase them onto electron/main.ts as a follow-up rather than keep two shells. One thing to compare: your background server outlives the tray's Quit; in #67 the relay ends with the app so a closed app never holds the port. Happy to coordinate either way.

…server

A plain Quit leaves the background server running so agents relaying
through it keep working. The new entry stops the server first and quits
only once the port is free; if the server will not stop, the shell stays
up and shows the error.
The installers' names, the Debian control file and the menu entry as plain functions, tested without
building an installer.
…p scripts

One place narrows Node's platform and architecture to the ones Electron publishes, and one reads a
package.json field.
A disk image on macOS (hdiutil), a setup program on Windows (Inno Setup) and a Debian package on
Linux (dpkg-deb), each the platform's own tool. The smoke test installs the result for real, runs
the installed app with --smoke, uninstalls it and checks it is gone.
Each runner makes the installer, installs it and runs the installed app, and uploads it beside the
archives, which a release attaches to the tag.
Which tool makes each, what the smoke test does, and the signing caveats.
@Mearman

Mearman commented Nov 1, 2026

Copy link
Copy Markdown
Author

Thanks for the note. One shell makes sense to me, and electron/main.ts plus electron/shell.ts are written to take features as separate modules (the shell is tested against a fake Electron, so a feature can be added the same way), so I'm happy for yours to land on top of it.

The background server outliving a plain Quit is deliberate: an agent that is mid-task keeps its session when the window closes. I've added a second tray entry, "Quit and stop background server", which stops the server first and quits once the port is free, so a closed app need not hold the port. A plain Quit still leaves it running, and scanruler-agent stop ends it from a terminal. That's on the feat/electron-shell branch of this PR's stack, and the PR is updated.

This branch has not been deployed

No deployments
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.

2 participants