Files
beardrive/cmd/bdrive/hooks.go
Snow LeeandClaude Fable 5 626a9c0a07 feat(cli): bdrive hooks — agent-agnostic sync hook registration
`bdrive hooks install` detects the agent platforms in use — Claude Code
(.claude/), Codex (.codex/), Gemini CLI (.gemini/), Hermes (~/.hermes/) —
and idempotently merges beardrive's turn-boundary sync hooks into each
platform's own hook config (JSON for claude/codex/gemini, YAML for
hermes), preserving existing hooks. All four pipe hook JSON with a
session_id, so one POSIX-sh hook command serves every platform: pull at
turn start, push after edits, changes stamped "<agent> session <id>".
Bare `bdrive hooks` prints the detection/registration table.

The beardrive skill now runs it automatically after `bdrive init`, and
/beardrive:install's hand-maintained settings.json block is replaced by
the command, so the hook content has one source of truth in the binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-11 10:57:07 -07:00

89 lines
2.7 KiB
Go

package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/runbear-io/beardrive/internal/agenthooks"
)
// bdrive hooks — register BearDrive's turn-boundary sync hooks with whatever
// AI agent platforms the user works with (Claude Code, Codex, Gemini CLI,
// Hermes). One command instead of hand-editing four config formats; the
// beardrive skill runs it right after `bdrive init`.
func hooksCmd() *cobra.Command {
c := &cobra.Command{
Use: "hooks",
Short: "Show which AI agent platforms have beardrive sync hooks registered",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
folder, err := absFolder(nil)
if err != nil {
return err
}
detected := map[string]bool{}
for _, a := range agenthooks.Detect(folder) {
detected[a] = true
}
for _, a := range agenthooks.Agents {
state := "not detected"
if detected[a] {
state = "detected, hooks not registered"
if agenthooks.Registered(folder, a) {
state = "hooks registered"
}
}
fmt.Printf(" %-8s %-32s %s\n", a, state, agenthooks.ConfigPath(folder, a))
}
fmt.Println("\nregister with: bdrive hooks install [--agent claude,codex,gemini,hermes]")
return nil
},
}
var agentsFlag string
install := &cobra.Command{
Use: "install [folder]",
Short: "Register sync hooks for detected agent platforms (or --agent list)",
Long: "Registers beardrive's sync hooks with each agent platform's own hook\n" +
"config: files pull before every turn and push after edits, and changes\n" +
"are stamped with the agent session that made them (`bdrive sync --note`).\n" +
"Merging is idempotent and preserves hooks you already have.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
folder, err := absFolder(args)
if err != nil {
return err
}
var agents []string
if agentsFlag != "" && agentsFlag != "auto" {
agents = strings.Split(agentsFlag, ",")
}
results, err := agenthooks.Install(folder, agents)
if err != nil {
return err
}
if len(results) == 0 {
fmt.Println("no agent platforms detected (looked for .claude/, .codex/, .gemini/ here or in ~; ~/.hermes/)")
fmt.Println("pick explicitly: bdrive hooks install --agent claude,codex,gemini,hermes")
return nil
}
for _, r := range results {
state := "already registered"
if r.Changed {
state = "registered"
}
fmt.Printf(" %-8s %s → %s\n", r.Agent, state, r.Path)
if r.Note != "" {
fmt.Printf(" note: %s\n", r.Note)
}
}
return nil
},
}
install.Flags().StringVar(&agentsFlag, "agent", "auto", "comma-separated platforms (claude,codex,gemini,hermes) or auto")
c.AddCommand(install)
return c
}