Files

160 lines
5.5 KiB
Go
Raw Permalink Normal View History

package main
import (
"fmt"
"slices"
"strings"
"github.com/spf13/cobra"
"github.com/runbear-io/beardrive/internal/agenthooks"
)
// hookAgents turns an --agent flag value into the list agenthooks takes,
// resolving EVERY name before either door writes anything. Install loops the
// names and returns on the first unknown one — after registering the ones
// before it — so argument order decided whether a command that reported
// failure had already changed the machine (`--agent claude,bogus` wrote a
// hook, `bogus,claude` did not). What it writes runs on every turn of every
// session on the machine, so a refused list registers nothing at all — the
// same posture `bdrive forget` takes with its arguments.
//
// An empty value is an explicit empty set, not "auto": an unexpanded shell
// variable must not be indistinguishable from "every platform detected here".
func hookAgents(flag string) ([]string, error) {
if flag == "auto" {
return nil, nil
}
if strings.TrimSpace(flag) == "" {
return nil, fmt.Errorf("--agent was given an empty value; name platforms (%s) or pass auto",
strings.Join(agenthooks.Agents, ", "))
}
agents := strings.Split(flag, ",")
for _, name := range agents {
if !slices.Contains(agenthooks.Agents, name) {
return nil, fmt.Errorf("unknown agent %q (supported: %s)", name, strings.Join(agenthooks.Agents, ", "))
}
}
return agents, nil
}
// 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("\nhooks are registered once per machine, in each platform's own user config —")
fmt.Println("they cover every session in every folder, and no project file is written.")
if detected["codex"] {
fmt.Println("\ncodex: hooks are experimental and off by default — enable them with")
fmt.Println(" [features] codex_hooks = true in ~/.codex/config.toml")
}
fmt.Println("\nregister with: bdrive hooks install [--agent claude,codex,gemini,hermes]")
fmt.Println("remove with: bdrive hooks uninstall")
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
}
agents, err := hookAgents(agentsFlag)
if err != nil {
return err
}
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.Migrated != "" {
fmt.Printf(" moved out of %s (project hooks are no longer used)\n", r.Migrated)
}
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")
var uninstallAgents string
uninstall := &cobra.Command{
Use: "uninstall",
Short: "Remove beardrive's sync hooks from every agent platform",
Long: "Removes only beardrive's own hook entries from each platform's user\n" +
"config, leaving any other hooks in those files untouched. Syncing itself\n" +
"is unaffected — the daemon keeps running; only turn-boundary sync stops.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
agents, err := hookAgents(uninstallAgents)
if err != nil {
return err
}
results, err := agenthooks.Uninstall(agents)
if err != nil {
return err
}
removed := 0
for _, r := range results {
if r.Changed {
removed++
fmt.Printf(" %-8s removed → %s\n", r.Agent, r.Path)
}
}
if removed == 0 {
fmt.Println("no beardrive hooks were registered")
}
return nil
},
}
uninstall.Flags().StringVar(&uninstallAgents, "agent", "auto", "comma-separated platforms (claude,codex,gemini,hermes) or auto")
c.AddCommand(install, uninstall)
return c
}