Why your .app can't find git or node
Almost every desktop app that shells out to a CLI hits this bug: it
works when you run it from a terminal, then a user double-clicks the
packaged app and git, node, mmdc, or a language server “isn’t
installed.” It is installed, but the app can’t see it.
The cause: a stripped PATH
When you launch an app from Finder (or a .desktop file on Linux), it does
not inherit your shell’s environment. It gets a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) with none of the directories your tools
actually live in: Homebrew (/opt/homebrew/bin), npm globals, and especially
anything under a version manager.
Editora shells out a lot, for Git, the Mermaid CLI, every LSP server, the debug adapters, and the run feature, so “can’t find the binary” would have been a constant support burden.
The fix: rebuild the PATH the app should have had
ProcessRunner is the one place Editora launches a subprocess, and it builds an
augmented PATH = the inherited PATH + the user’s login-shell PATH + the
usual install dirs, then rewrites a bare command name (git) to its absolute
path against that PATH before launching. (Java’s Unix ProcessBuilder resolves
the executable against the JVM’s own stripped PATH, not the child env, so both
the augmentation and the rewrite are needed.)
The piece that matters most is the login-shell PATH. Editora runs your shell once, as $SHELL -l -i -c 'printf …$PATH…', with stdin from /dev/null, stderr
discarded, a 5-second timeout, and marker strings fencing the PATH off from any
.zshrc banner output (the pure marker-parse is unit-tested). This is the same
approach VS Code uses (resolveShellEnv).
Hardcoding directories isn’t enough, because the important
ones are version-specific and can’t be hardcoded: nvm’s
~/.nvm/versions/node/<version>/bin (where npm-global tools like
typescript-language-server and pyright-langserver live) changes with every
Node version, and fnm/asdf/volta are similar. Only your shell knows the current
one. A list of hardcoded dirs (/opt/homebrew/bin, ~/.cargo/bin, ~/go/bin,
…) remains as a fallback.
Making it cheap
Spawning a login shell isn’t free, so the result is cached after the first call, and the LSP manager warms it off-thread at startup, so neither tool detection nor the first subprocess launch blocks the UI. (Windows has no POSIX login-shell convention, so it skips this and relies on the hardcoded dirs plus any Settings override.)
If your app shells out too
If your app shells out and “works from the terminal but not when double-clicked,” this is almost certainly why. Reconstruct the PATH the user has in their shell instead of trusting the one a GUI launch hands you, and resolve binaries to absolute paths yourself.