Homebrew tap installer, dropbear roots CLI, and tray daemon lifecycle control #22

Merged
erik merged 8 commits from add-installer-and-roots-cli into main 2026-07-03 04:30:41 +00:00
Owner

Polishes the bootstrap story in two workstreams (plus the tray lifecycle commit this branch builds on):

Roots management — new dropbear roots list|add|remove backed by internal/rootsedit, a comment-preserving daemon.toml editor (raw-byte span surgery; go-toml/v2 has no document model). Every candidate is reparse-validated before an atomic write. add carries the [roots.init] bootstrap flags shared with register; --restart bounces the daemon via daemonctl. The CLI sources the daemon env file as fallback so $DROPBEAR_DEVICE_ID-style indirection validates outside the service environment. daemonconfig exports Parse, NormalizePath, StarterTemplate for reuse by the future status-API endpoint.

Homebrew installer — org tap tfks/homebrew-tap (tap as tfks/tap, formula builds both binaries from the release source tarball; the tray needs CGo so there are no cross-compiled bottles). just build::dist replaces the nonexistent build-release target release.yaml called: per-platform daemon tarballs, git-archive source tarball, checksums.txt. A post-publish step bumps the formula url/sha256 using the TAP_TOKEN secret (warns and skips when unset).

Fixed en route:

  • darwin CGO_ENABLED=0 cross-builds broken since the FSEvents backend (now darwin && cgo, fsnotify fallback)
  • daemon install pinned brew's versioned Cellar path into the plist (StableBinaryPath keeps the prefix symlink)

Docs: new wiki/runbooks/homebrew-install.md + wiki/runbooks/release.md; install-daemon, README, PROJECT.md updated.

Verified: full test suite (race), lint, gitleaks; roots roundtrip byte-clean against a copy of the real config; formula validated with brew audit + real --HEAD install + brew test.

🤖 Generated with Claude Code

Polishes the bootstrap story in two workstreams (plus the tray lifecycle commit this branch builds on): **Roots management** — new `dropbear roots list|add|remove` backed by `internal/rootsedit`, a comment-preserving daemon.toml editor (raw-byte span surgery; go-toml/v2 has no document model). Every candidate is reparse-validated before an atomic write. `add` carries the `[roots.init]` bootstrap flags shared with `register`; `--restart` bounces the daemon via `daemonctl`. The CLI sources the daemon env file as fallback so `$DROPBEAR_DEVICE_ID`-style indirection validates outside the service environment. `daemonconfig` exports `Parse`, `NormalizePath`, `StarterTemplate` for reuse by the future status-API endpoint. **Homebrew installer** — org tap `tfks/homebrew-tap` (tap as `tfks/tap`, formula builds both binaries from the release source tarball; the tray needs CGo so there are no cross-compiled bottles). `just build::dist` replaces the nonexistent `build-release` target release.yaml called: per-platform daemon tarballs, git-archive source tarball, checksums.txt. A post-publish step bumps the formula url/sha256 using the `TAP_TOKEN` secret (warns and skips when unset). Fixed en route: - darwin `CGO_ENABLED=0` cross-builds broken since the FSEvents backend (now `darwin && cgo`, fsnotify fallback) - `daemon install` pinned brew's versioned Cellar path into the plist (`StableBinaryPath` keeps the prefix symlink) Docs: new `wiki/runbooks/homebrew-install.md` + `wiki/runbooks/release.md`; install-daemon, README, PROJECT.md updated. Verified: full test suite (race), lint, gitleaks; roots roundtrip byte-clean against a copy of the real config; formula validated with `brew audit` + real `--HEAD` install + `brew test`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replace the kqueue/fsnotify backend on Darwin with a native FSEvents
stream that watches the root recursively in a single call. Confirmed
with lsof: fd count is constant regardless of file count under the root,
eliminating the EMFILE failure on large trees.

Introduces a build-tag-selected `backend` interface inside
`internal/fswatch`; the platform-neutral shell (debounce, ignore
filtering, shelve suppression, single-slot wakeup) drives whichever
backend is compiled in:
  - //go:build !darwin: existing fsnotify logic, unchanged semantics
  - //go:build darwin:  github.com/fsnotify/fsevents v0.2.0, one
    recursive EventStream per root, no per-directory subscription growth

Key correctness details:
  - Root-directory events (rel == ".") suppressed — FSEvents emits
    them as a side-effect of child changes; the child's event is the
    real signal
  - shouldEmitPath now walks ancestor dirs so files inside ignored
    dirs (node_modules/ etc.) are filtered even with FSEvents'
    full-subtree visibility
  - filepath.EvalSymlinks at New time normalises /var → /private/var
    so reported paths feed relPath correctly

Also adds 7 tag-free shell unit tests (fake backend) and 4 Darwin
end-to-end tests including a 300-file EMFILE regression. Closes the
"macOS file-watcher exhausts file descriptors" entry in doc/gaps.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tray's top item was a stub that only logged intent. Wire it to a new internal/daemonctl package that starts and stops the installed daemon service through the OS service manager: launchd on macOS, systemd --user on Linux, and an unsupported no-op elsewhere. Dispatch is on runtime.GOOS (mirroring internal/daemoninstall), no build tags, since control is just argv handed to os/exec.

Stop is persistent (launchctl disable + bootout; systemctl --user disable + stop) so a manual stop survives login and is not relaunched by launchd's RunAtLoad or systemd's enabled symlink. Start clears the disable and loads-or-restarts (bootstrap-or-kickstart on launchd; enable + restart on systemd). Start returns a typed ErrNotInstalled when the unit/plist is absent, and the tray logs guidance to run `dropbear daemon install` rather than installing anything itself.

The toggle is optimistic: on click it flips the line to the requested direction and disables the item, runs the service-manager call off the main goroutine, and lets the next status poll re-enable and reconcile it. Any lifecycle error is logged and left for the poll — it never crashes the tray.

Exposes label/unit-path accessors from internal/daemoninstall so daemonctl sources them rather than re-hardcoding. Docs updated (desktop-tray feature page, gaps) and an ADR added at wiki/decisions/daemon-lifecycle-control.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds internal/rootsedit, a comment-preserving editor for the [[roots]] list in daemon.toml: Add appends a rendered block, Remove deletes the matching table span (taking the comment block attached above its header), and every candidate is validated by reparsing through daemonconfig before an atomic write. go-toml/v2 has no document model, so raw-byte span surgery is what keeps user comments intact.

The new `dropbear roots` command exposes list (table or --json), add (with optional [roots.init] bootstrap flags mirroring register via a shared --remote-* helper), and remove. After an edit, a running daemon triggers a restart hint; --restart stops and starts it via daemonctl. daemonconfig now exports Parse, NormalizePath, and StarterTemplate (moved from daemoninstall) so the editor and future status-API endpoints reuse the same validation and starter content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
release.yaml called `just build-release`, a recipe that never existed, so tag builds failed at the build step. The new build::dist recipe produces build/dist/ with per-platform tarballs of the CGo-free daemon (dropbear_<ver>_<os>_<arch>.tar.gz for darwin_arm64, linux_amd64, linux_arm64), a git-archive source tarball for the upcoming Homebrew formula, and checksums.txt; the workflow now calls it and uploads build/dist.

Cross-compiling darwin with CGO_ENABLED=0 had been broken since the FSEvents hint backend landed: fsnotify/fsevents needs CGo but backend_fsevents.go was gated only on darwin. The backends are now tagged darwin&&cgo / !darwin||!cgo, so CGo-less darwin builds fall back to the kqueue-based fsnotify backend while native (and brew-built) binaries keep FSEvents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
daemon install resolved the binary path with EvalSymlinks before rendering the plist/unit, which under Homebrew pins the versioned Cellar path — deleted on every brew upgrade, silently breaking the service. StableBinaryPath (applied both in the CLI and inside Install) still resolves symlinks but keeps the caller's path when resolution lands inside a Cellar the original path was not already in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After publishing release assets, the workflow clones tfks/homebrew-tap with the TAP_TOKEN secret and rewrites the formula's url/sha256 to the new source tarball, using the checksum computed by build::dist. When the secret is absent the step warns and skips, so rc tags can exercise the pipeline before the token is provisioned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New runbooks: homebrew-install (user path: tap, install, daemon bootstrap, upgrade semantics around the stable prefix symlink) and release (maintainer path: just release → CI artifacts → tap bump, TAP_TOKEN provisioning and failure modes). install-daemon now points at the brew path and the roots CLI instead of hand-editing daemon.toml; README gains an Install section; PROJECT.md records the change and the deferred follow-ups (config hot-reload, Windows platform impls).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Load the daemon env file as fallback environment for roots CLI validation
All checks were successful
Validate / validate (pull_request) Successful in 2m13s
424ccbdec6
roots add/list/remove reparse daemon.toml, which expands $VAR references — but variables like DROPBEAR_DEVICE_ID live in the daemon's env file, not the operator's shell, so editing a config that uses the recommended env-var indirection failed validation. The CLI now loads the env file sibling to the config (existing process env wins) before parsing, mirroring the service manager's `set -a; . env`. CheckEnvFile's line parser is extracted as daemoninstall.ParseEnvFile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erik force-pushed add-installer-and-roots-cli from 424ccbdec6
All checks were successful
Validate / validate (pull_request) Successful in 2m13s
to 2fb499c8c8
All checks were successful
Validate / validate (pull_request) Successful in 54s
2026-07-03 04:29:36 +00:00
Compare
erik merged commit c02515bb7f into main 2026-07-03 04:30:41 +00:00
erik deleted branch add-installer-and-roots-cli 2026-07-03 04:30:41 +00:00
Sign in to join this conversation.
No description provided.