Make the shipped app work on machines that did not build it - #3
Merged
Conversation
1.1.1 was correct as source and as an npm package. Its DMG was not. Every build embedded the absolute path of its own node binary and CLI. That is right for a developer whose installed app should drive the checkout they are editing, and wrong for a release: the 1.1.1 DMG shipped `TokenFlowCLIPath = /Users/runner/work/tokenflow/tokenflow/bin/tokenflow.js`, a path that exists on no user's machine. Only the discovery fallbacks kept the app working at all, and only for people who had installed the CLI from npm. A distributable build now embeds no path at all (TOKENFLOW_PORTABLE=1, set by build-dmg.sh). Three things enforce it rather than one: the DMG script refuses to package a bundle carrying either key, the release workflow re-checks the finished image after mounting it, and a test builds both flavours and asserts the difference. A build-script property is invisible to every other test, so only a real build can prove it. Second half of the same problem: the cask installs the app and the CLI came from npm, so anyone who only ran `brew install --cask tokenflow` had a menu bar that could read an existing status file and nothing else — no refresh, no watcher, no dashboard. The app now carries its own CLI at Contents/Resources/cli, packed with `npm pack` and then pruned to the files the CLI executes; the bundle is therefore always a subset of the published package and can never contain a file npm does not ship. Pruning docs/, skills/, examples/ and scripts/ — none of which is read at runtime, they appear in printed hints and nothing opens them — takes the DMG from 2.5 MB to 932 KB. The bundled copy wins over any CLI found on the system. The app and the CLI share a contract (the status file's shape, the watcher lock format, /api/ping), so the copy shipped beside the binary is the only one guaranteed to match it; a separately installed CLI may be newer without being compatible. A local build still embeds the developer's clone and that still wins, so the dev loop is unchanged. Node discovery no longer names a version. The first candidate was ~/.nvm/versions/node/v24.13.1/bin/node — whichever version the developer happened to have, one `nvm install` from being wrong for everybody. Installs are discovered, anything below the engine floor is skipped, and the highest wins by semver rather than by string order: verified against a tree where v24.13.0 must beat v24.9.0, which a lexicographic sort gets backwards. Finally, a missing dependency says so. With no CLI or no Node every button failed in silence; the popover now names the problem and the command that fixes it, at launch rather than on the first click. The cask states the Node requirement in caveats rather than declaring `depends_on formula: "node"`, which would plant a second Node beside an nvm- or asdf-managed one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1.1.1 was correct as source and as an npm package. Its DMG was not.
The app pointed at the machine that built it
Every build embedded the absolute path of its own node binary and CLI — right for a developer whose installed app should drive the checkout they are editing, wrong for a release. The shipped 1.1.1 DMG contains:
Neither exists on any user's machine. Only the discovery fallbacks kept the app working, and only for people who had installed the CLI from npm.
A distributable build now embeds nothing (
TOKENFLOW_PORTABLE=1, set bybuild-dmg.sh). Three things enforce it, not one:build-dmg.shrefuses to package a bundle carrying either key.test/bundle.test.jsbuilds both flavours and asserts the difference.A build-script property is invisible to every other test, so only a real build can prove it. These tests do real builds and skip where
swiftcis unavailable.A cask-only install had nothing to run
The cask installs the app; the CLI came from npm.
brew install --cask tokenflowalone gave you a menu bar that could read an existing status file and do nothing else — no refresh, no watcher, no dashboard.The app now carries its own CLI at
Contents/Resources/cli, packed withnpm packand then pruned to what the CLI executes. Packing rather than copyingbin/ src/means the bundle is always a subset of the published package and can never hold a file npm does not ship. Pruningdocs/,skills/,examples/,scripts/— verified as never read at runtime; they appear in printed hints and nothing opens them — keeps the download honest:The bundled copy wins over any CLI found on the system. The app and the CLI share a contract — the status file's shape, the watcher lock format,
/api/ping— so the copy shipped beside the binary is the only one guaranteed to match it. A separately installed CLI may be newer without being compatible. A local build still embeds the developer's clone and that still wins, so the dev loop is unchanged; a test asserts that too.Node discovery named one specific version
The first candidate was
~/.nvm/versions/node/v24.13.1/bin/node— whichever version the developer happened to have, onenvm installfrom being wrong for everyone. Installs are now discovered, anything below theenginesfloor is skipped, and the highest wins by semver, not string order — verified against a tree wherev24.13.0must beatv24.9.0, which a lexicographic sort gets backwards:A missing dependency now says so
With no CLI or no Node, every button failed in silence. The popover names the problem and the command that fixes it, at launch rather than on the first click.
The cask states the Node requirement in
caveatsrather than declaringdepends_on formula: "node", which would plant a second Node beside an nvm- or asdf-managed one and fight the version manager.Verification
doctor,demo,usage,watch --once,watch --status— to prove the pruning cut nothing it loads.The cask still points at 1.1.1 on purpose — it gets the new version and checksum once the 1.1.2 DMG exists.
🤖 Generated with Claude Code