Skip to content

feat(app): self-contained installer app — frozen CLI + one-click onboard - #75

Open
mikehasa wants to merge 2 commits into
mainfrom
feat/packaging-frozen-cli
Open

feat(app): self-contained installer app — frozen CLI + one-click onboard#75
mikehasa wants to merge 2 commits into
mainfrom
feat/packaging-frozen-cli

Conversation

@mikehasa

@mikehasa mikehasa commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Ships a self-contained installer app so a non-developer can install agentacct without Python, pipx, or a terminal. The app embeds a frozen standalone CLI and drives the normal agentacct onboard from a one-click screen. No core data/counting paths change.

Why

The app is a GUI over the daemon; it cannot replace the CLI. The MCP servers the coding agents connect to (agentacct mcp serve), the daemon (agentacct serve), and the recording hooks are all the Python CLI. So a machine with no Python needs the CLI as a standalone binary. This freezes it and ships it in the app.

Packaging (packaging/)

  • freeze-cli.sh — PyInstaller onedir freeze (~0.5s warm start; onefile re-extracts 24MB per launch, unusable for a CLI launched by every hook/MCP connection). The frozen binary is a drop-in for a pip/pipx agentacct.
  • pyinstaller_entry.py — the frozen entry. Besides the CLI it emulates the two Python-interpreter forms the Claude Code hooks invoke it with (<cli> -m agentacct.statusline_hook, <cli> <hook>.py), so one binary is CLI + hook runner with zero change to onboard or to existing pip/pipx installs.
  • build-dmg.sh — freeze → build app (embeds CLI) → sign → DMG → notarize. Signing/notarization are env-gated (DEVELOPER_ID / NOTARY_PROFILE): the same script makes an unsigned DMG today and a signed+notarized one once a Developer ID exists — no code change.
  • entitlements.plist — hardened-runtime entitlements a frozen Python binary needs to run notarized.

apps/agentacct/Scripts/build-app.sh embeds the frozen CLI into Contents/Resources/cli/ when present; dev builds skip it and stay fast.

App setup flow

SetupModel / SetupSheet:

  1. Copy the embedded CLI → ~/.local/share/agentacct/cli/ (stable; an app upgrade replaces it in place so every registration stays valid).
  2. Write a ~/.local/bin/agentacct wrapper on PATH.
  3. Run agentacct onboard --agent auto --yes from the installed binary, which registers MCP servers, hooks, and standing instructions and stamps the stable installed path into every config.

A packaged build whose recorder isn't installed offers setup once on first launch; a dev build (no embedded CLI) never prompts.

Verified end-to-end

  • Frozen binary runs mcp serve (MCP initialize + tools capability), the daemon (HTTP 200), onboard (writes CLAUDE.md / settings.json / .claude.json / hooks), and both hook forms (statusline -m, SessionStart script.py).
  • The install simulation writes stable installed paths into every hook, and those hook commands run via the installed binary.
  • build-dmg.sh produces a 29MB DMG; mounting it and running the app's embedded CLI works (--version, mcp serve).

Not in scope (blocked on Apple Developer account)

Code signing + notarization — wired and env-gated, runs once DEVELOPER_ID + NOTARY_PROFILE are set. Until then the DMG is unsigned (first launch needs right-click → Open).

…ck onboard

A non-developer can now install agentacct without Python, pipx, or a
terminal. The app embeds a standalone CLI and drives the normal
`agentacct onboard` from a one-click screen.

Packaging pipeline (packaging/):
- freeze-cli.sh freezes the Python CLI into a PyInstaller onedir binary
  (~0.5s warm start). It is a drop-in for a pip/pipx agentacct: the CLI,
  the uvicorn/fastapi daemon, the MCP stdio server, and onboard all run
  frozen. The interpreter-emulation entry (pyinstaller_entry.py) also
  handles the two Python forms the Claude Code hooks invoke it with
  (`<cli> -m module`, `<cli> script.py`), so one binary is CLI + hook
  runner with no change to onboard or existing installs.
- build-dmg.sh: freeze -> build app (embeds the CLI) -> sign -> DMG ->
  notarize. Signing and notarization are env-gated (DEVELOPER_ID /
  NOTARY_PROFILE), so the same script makes an unsigned DMG today and a
  signed+notarized one once a Developer ID exists — no code change.
- entitlements.plist carries the hardened-runtime entitlements a frozen
  Python binary needs to run notarized.

App:
- build-app.sh embeds packaging/dist/agentacct into Contents/Resources/cli
  when present (dev builds skip it and stay fast).
- SetupModel installs the embedded CLI to ~/.local/share/agentacct/cli,
  writes a ~/.local/bin/agentacct wrapper on PATH, and runs onboard from
  the installed binary so every hook/MCP config points at the stable
  installed path. SetupSheet is the one-click screen; a packaged build
  whose recorder isn't installed offers it once on first launch.

Verified end-to-end: the frozen binary runs mcp serve (MCP initialize +
tools), the daemon, onboard (writes all client configs), and both hook
forms; the install flow writes stable paths; the DMG (29MB) mounts and
its embedded CLI runs.

Author: mikehasa
…ning script

- ProcessRunner: the EOF (empty-read) branch now drains the final
  unterminated tail once and nils its own handler, instead of leaving the
  handler installed to re-fire on every empty read (busy-spin + duplicate
  tail line). terminationHandler still only finishes — buffer stays owned
  by the readability queue, no cross-queue data race.
- build-dmg.sh: the embedded-CLI codesign pass no longer swallows stderr
  (a lib that won't sign must surface, not fail notarization silently);
  dropped the ineffective post-DMG app staple (it stapled a stage dir not
  in the shipped DMG).

Author: mikehasa
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.

1 participant