Commit Graph
5 Commits
Author SHA1 Message Date
Snow Lee (Sungwon)andGitHub 118abc67e2 feat(cli): "bdrive serve" replaces "bdrive web" (web stays as an alias), and the README leads with the agent install (#94) 2026-07-30 17:46:58 +09:00
fb6ce347c4 feat(daemon): survive a reboot — login autostart on macOS/Linux/Windows, and a lock instead of a pidfile (#88)
* feat(daemon): bring sync back after a reboot, and stop trusting the pidfile

A reboot killed every daemon and nothing restarted them. Agent hooks still
synced per turn, which is what made it easy to miss: a folder looked fine
while an agent worked in it and went stale the moment one didn't. `bdrive
init` now registers a login item (macOS: a user LaunchAgent) that runs the
new `bdrive resume` — one registration per machine, which starts a daemon for
every enrolled, unpaused mount, so adding a project later needs no
re-registration and `bdrive stop` still means stay stopped. `--no-autostart`
opts out, `bdrive autostart install|uninstall` manages it.

Writing the plist is the whole job: no `launchctl` shell-out. launchd loads
agents at login anyway, the caller has just started the daemon for this
session, and shelling out would let a test or a packaging script register a
real login item as a side effect.

The recovery path was also broken, which is why this is one change. Liveness
came from `kill(pid, 0)` on daemon.pid — but that file lives in
$BDRIVE_HOME and survives the reboot that killed its process, so any
same-user process recycling the pid read as a live daemon. `bdrive status`
said "running", and worse `daemon.Start` returned early, so the one
documented recovery (`bdrive init`) reported success and started nothing.
Liveness is now an flock held for the daemon's lifetime: the kernel drops it
at death or reboot, and it makes two daemons on one mount impossible. The pid
stays for display and for signalling.

internal/autostart is darwin-only today; autostart_other.go returns
ErrUnsupported and every caller already treats that as "nothing to do", so
Linux (systemd user unit) and Windows are one file each.

Tests: internal/daemon gets its first ones — a recycled pid must not read as
running (the exact regression), the lock decides liveness, a second holder is
refused. internal/autostart covers write/idempotency/stale-path-rewrite/
uninstall with HOME redirected, and lints the plist with plutil so launchd
can actually parse it. The CLI e2e asserts init registers the agent, that it
runs `resume`, that resume finds the live daemon instead of starting a
second, and that --no-autostart is silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Linux support — a systemd user unit alongside the launchd agent

Same three functions, same discipline. Linux writes
$XDG_CONFIG_HOME/systemd/user/beardrive.service (Type=oneshot, no Restart= —
`bdrive resume` exits by design) plus the default.target.wants symlink that
`systemctl --user enable` would create, because systemd ignores a unit
nothing wants. No `systemctl` shell-out, for the same reasons as launchctl:
the file is the registration, it only matters at the next login, and a
container or ssh session has no session bus to talk to.

Install declines with ErrUnsupported unless systemd is actually the init
system (/run/systemd/system, i.e. sd_booted) — on Alpine, WSL1 or a slim
container a unit file is inert decoration, and reporting "registered" would
be a lie. Installed() likewise requires the enable symlink, not just the
unit: a unit nothing wants never starts.

os.UserConfigDir honors XDG_CONFIG_HOME, so relocated config dirs work.
Windows is now the only gap; autostart_other.go is !darwin && !linux, and the
shared writeIfDifferent/selfPath moved into the tag-free autostart.go (darwin
now uses them too).

Tests run on Linux, not just compiled for it: cross-compiled test binaries
executed in a container, both with /run/systemd/system present (unit written,
enabled, idempotent, stale ExecStart rewritten, broken symlink repaired,
XDG honored, uninstall removes both) and without it (Install declines and
writes nothing). The daemon flock tests were run there too, since flock
semantics are per-OS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

* feat(autostart): Windows support — a per-user Run entry

Third platform, same three functions. Windows has no user service manager in
the launchd/systemd sense, so the registration is a HKCU\...\Run value via
golang.org/x/sys/windows/registry (already in the module graph; go mod tidy
just promotes it to direct).

Chosen over the alternatives for the same reason the other two write files:
no admin rights, no COM (a Startup-folder .lnk needs it), no schtasks
shell-out. It is also honestly discoverable — the entry appears in Task
Manager's Startup tab, where someone can disable it without knowing bdrive
exists. The executable is quoted because Explorer parses the value as a
command line and Program Files has a space in it.

Two things a reader should not have to discover for themselves:

- The tests here have NEVER RUN. They are written and compile-checked
  (GOOS=windows go test -c) from macOS; there is no Windows host or usable
  container on an arm64 mac. They execute the first time the suite runs on
  Windows. They also cannot use a temp HOME the way the macOS and Linux tests
  do — HKCU is real — so each one snapshots and restores the previous value.
- `GOOS=windows go build ./...` still does not pass, and this package is not
  why: internal/store's Lock uses syscall.Flock and internal/daemon uses
  syscall.Kill and Setsid, all unix-only (true before this branch too). A
  Windows port means LockFileEx plus a stop story for a platform with no
  SIGTERM — a separate change, against the sync invariants, and untestable
  from here. So this code is correct and currently unreachable.

autostart_other.go is now !darwin && !linux && !windows (the BSDs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:32:13 +09:00
31f705e287 chore: drop the Claude plugin and the bundled skill — hooks are the integration (#85)
Two front doors for the same setup, and one of them was a second copy of
everything. The plugin shipped the skill the CLI already installs (to four
platforms, not one), hooks that ran the identical commands `bdrive hooks
install` writes machine-level, and an install flow duplicating
INSTALL_FOR_AGENTS.md. Nothing deduped, so a machine with both pulled twice
per turn — two blocking syncs — and showed two identical `beardrive` skills
in the picker.

What remains is `internal/agenthooks` plus the runbook: init registers a
blocking pull (which also injects the gated-link convention as
additionalContext), an async push on Write/Edit, and read-log for the
heatmap, in each platform's user config, once per machine. That is the whole
integration, and it is the part that was never optional.

Removed: plugin/, .claude-plugin/marketplace.json, internal/agentskills,
`bdrive skill`, `bdrive hook-approve` (its PreToolUse auto-approve only ever
helped when a plugin pre-installed it; the substitute is a `Bash(bdrive:*)`
permission entry, which is user-owned config and needs no code).

The e2e now asserts the absence: no SKILL.md in any platform's skills dir
after init, and no `skill` subcommand. login_test keeps the "no revoke
surface" wording check on logoutNote alone.


Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:40:47 +09:00
6628718f1d fix(cli): agent hooks never sync folders this device didn't opt into (#44)
* fix(cli): agent hooks never sync folders this device didn't opt into

The turn hooks decided "this folder is managed" from the mere presence of
.bdrive/config.json — a file designed to travel with the folder. Two holes:

- A config.json arriving via git clone / copied dir made one hook firing
  silently mint a device identity, register the mount, create a volume
  store, journal the whole folder, and inject the hub-link formula — on a
  device that never ran init or login.
- `bdrive stop` only killed the daemon: the next agent turn's
  `bdrive sync --hook` resumed a full sync cycle and kept injecting links,
  and `stop --forget` was undone within one turn by registry self-heal.

Fix: one gate (`syncBlocked`) in the paths all hooks route through —
sync/sync --hook/read-log now require the mount to already be enrolled in
this device's mounts.json (read without ResolveMount's enrolling
self-heal) and not paused. Hook mode exits silently; plain `bdrive sync`
errors with a `bdrive init` pointer. New per-device paused marker in the
volume dir: set by `bdrive stop`, cleared by `bdrive init` (startSync).
Only init enrolls or resumes; folder moves still self-heal since
enrollment is keyed by mount id, not path.

Docs updated (README, SKILL.md, docs cli reference, CHANGELOG). Tests:
hook/read-log no-op + no-enrollment on unenrolled and paused mounts,
plain-sync refusals, stop→pause→forget regression, paused marker contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: architecture-diagram PRs must show before/after excerpts of changed classes

The "Architecture changes" PR section now names exactly what changed and
shows Before and After mermaid excerpts scoped to the affected classes and
their immediate relationships — never the full diagram (Before = merge
base). Convention updated in CLAUDE.md, architecture/README.md, and the
pre-PR hook's reminder text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add cli-sync architecture diagram; widen diagram convention to the CLI

architecture/cli-sync.md draws the CLI and sync engine (cmd/bdrive +
internal/{syncer,store,journal,config,daemon,agenthooks}): the Session
cycle over Store/journal/remote, and the command layer with the new
syncBlocked opt-in gate, paused marker, and enrollment ownership. The
pre-PR hook and CLAUDE.md now watch these packages too, so CLI-side
structural changes trigger the before/after-excerpt convention the same
way server changes do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: full-coverage architecture diagrams — overview, frontend, agentskills

Every application package is now drawn somewhere: overview.md (system
diagram — package map, device↔hub↔storage flow, agent surfaces, and the
private cloud/ repo as an external seam consumer), webapp-frontend.md (the
hub SPA's modules: App/HubApp/VolumeApp/Browser, the in-repo nav/router,
api layer, hooks, components), and agentskills added to cli-sync.md. The
pre-PR hook now watches all of cmd/, internal Go code, and frontend/src
(generated static/ excluded); CLAUDE.md and architecture/README.md state
the coverage rule: every code change lands in exactly one detail diagram's
scope, web/docs and cloud/ deliberately excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: PR bodies start with a TL;DR — max 5 informal one-liners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:53:23 +09:00
2b9beaf41a docs: architecture class diagrams + keep-updated-in-PRs workflow (#40)
architecture/ holds mermaid class diagrams of the bdrive web server: the
Server core with its seams (Source, AuthProvider, Directory, QuotaProvider,
remote.Backend) and the MetaStore persistence layer.

Convention (CLAUDE.md): a PR that changes the drawn structure updates the
affected diagram in the same branch and embeds only the changed diagrams'
mermaid blocks in the PR description. A PreToolUse hook on gh pr create
(.claude/hooks/check-arch-diagrams.sh) blocks PR creation when
internal/webapp or internal/remote changed but architecture/ didn't;
override with '# skip-diagram-check' when nothing structural changed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:23:18 -07:00