Skip to content

elev8tion/macapp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mkmacapp

Turn any localhost web app into a real macOS app.

You have a project that runs on localhost:3000. mkmacapp wraps it in a native .app bundle — its own icon, its own Dock entry, its own window and menu bar. No browser tab, no address bar, no remembering which port it was on.

cd ~/my-project
mkmacapp init          # writes macapp.json, guessing from your stack
mkmacapp --install     # builds it and drops it in /Applications

That's the whole workflow. Your app is now in Spotlight and your Dock.

What it actually builds

A .app bundle containing a small Swift/WKWebView launcher. On open it picks a port, starts your server on it, waits for the port to accept connections, then loads your UI in a native window. On quit it terminates the server.

There is no Electron here, and no bundled browser engine. The launcher is ~300 lines of Swift against the system WebKit, so a bundle adds a few hundred kilobytes rather than a hundred megabytes.

Requirements

macOS, and Xcode Command Line Tools for swiftc:

xcode-select --install

Install

git clone https://github.com/elev8tion/macapp.git
cd macapp
./install.sh            # into ~/.local/bin
./install.sh --link     # or symlink, so `git pull` updates the tool

Three modes

mkmacapp init picks one for you based on the project. Override it in macapp.json if the guess is wrong.

command — run the project in place

Best for Node, Python, Bun, or anything with a dev server. Nothing is copied into the bundle; the app runs your project where it already lives.

{
  "name": "notes",
  "command": "npm run dev",
  "port_env": "PORT",
  "addr_template": "{port}"
}

Your node_modules stays where it is, and edits to the project are live the next time you open the app — no rebuild. The trade-off: move or delete the project directory and the app stops working.

build — compile a self-contained binary

Best for Go and Rust. The binary is built and copied into the bundle, so the app keeps working even if the source tree is gone.

{
  "name": "ledger",
  "build": "go build -o {out} ./cmd/app",
  "port_env": "ADDR",
  "addr_template": "127.0.0.1:{port}",
  "resources": ["static", "templates"]
}

Anything read from disk at runtime must be listed in resources (copied into Contents/Resources, which is the server's working directory) or embedded in the binary.

exec — bundle an existing launcher

For when you already have an executable or a launch script.

{
  "name": "dashboard",
  "exec": "launch.sh",
  "resources": ["server.js", "public"],
  "port_env": "",
  "args": ["--port", "{port}"]
}

Configuration

The only field that always needs your attention is how your server takes its port. Read your own startup code and match it:

Your server reads Set
process.env.PORT "port_env": "PORT", "addr_template": "{port}"
os.Getenv("ADDR") "port_env": "ADDR", "addr_template": "127.0.0.1:{port}"
a --port flag "port_env": "", "args": ["--port", "{port}"]

Everything else has a default that works:

Field Default Notes
preferred_port "auto" Assigns a stable port from ~/.mkmacapp/ports.json (41000+) on first build and keeps it. URLs survive restarts; two apps never collide. A number pins it, 0 forces a random port.
health_path "" Empty waits for the port to accept a TCP connection — correct regardless of routes, redirects, or auth. Set a path only if your port opens before the app can serve.
icon assets/icon.png Any square PNG ≥512px. If the file is missing, an icon is generated from the app name — a colored tile with its initials, hue derived from the name so every app looks distinct.
start_path "/" The route the window opens on.
window [1280, 840] Initial size. The window remembers its position after that.
identifier local.<name>.app Bundle identifier.
cwd project dir command mode only — where to run the command.

What the window gives you

The launcher is not a bare web view. It wires up the things that silently break otherwise:

  • File pickers<input type="file"> renders nothing in a plain WKWebView without a delegate.
  • DownloadsContent-Disposition: attachment is dropped silently without a download delegate. Files land in ~/Downloads and get revealed in Finder.
  • JS dialogsalert, confirm, and prompt become native panels.
  • Menu bar — with working Cut/Copy/Paste/Select All and ⌘R to reload.
  • Clean shutdown — quitting terminates the server. The server is also handed a pipe on stdin, so a server that watches stdin can shut itself down even if the app is force-quit.

Caveats

  • Ad-hoc signed. These bundles are signed with codesign -s -, which is fine on the machine that built them. Sending one to someone else needs a Developer ID and notarization — out of scope here.
  • command mode is not portable. It points at a path on your disk. Use build mode for anything you want frozen.
  • Finder does not read your shell profile. command mode bakes the build-time PATH into its launcher so node, python3, and friends resolve. A hand-written exec script has to handle that itself.
  • White window on launch means the server never started listening. Run your start command in a terminal with the port set the same way and read the error.

Using it with Claude Code

skills/macapp/SKILL.md is a Claude Code skill. Install it with ./install.sh --skill, then say /macapp in a project and Claude will read how the server starts and write macapp.json for you.

License

MIT

About

Turn any localhost web app into a real macOS app — native window, Dock icon, no Electron.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages