feat(setup claude): add --stealth flag
Add a `--stealth` flag to `bd setup claude` that installs Claude Code hooks using `bd prime --stealth` instead of `bd prime`. This extends the stealth workflow introduced for `bd prime` to the setup command, enabling workflows where git operations should be deferred or handled separately from bd database flushing. When `--stealth` is specified, the installed hooks call `bd prime --stealth`, which outputs only `bd sync --flush-only` in the close protocol, omitting all git operations. Update `RemoveClaude()` to remove both command variants (`bd prime` and `bd prime --stealth`) for backwards compatibility with existing installations. Update `hasBeadsHooks()` to detect either variant as a valid installation. Add comprehensive test coverage for stealth mode: hook installation with stealth command, removal of both variants, detection of both variants, and idempotency with stealth mode. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ var (
|
|||||||
setupProject bool
|
setupProject bool
|
||||||
setupCheck bool
|
setupCheck bool
|
||||||
setupRemove bool
|
setupRemove bool
|
||||||
|
setupStealth bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
@@ -86,7 +87,7 @@ agents from forgetting bd workflow after context compaction.`,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setup.InstallClaude(setupProject)
|
setup.InstallClaude(setupProject, setupStealth)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ func init() {
|
|||||||
setupClaudeCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (not globally)")
|
setupClaudeCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (not globally)")
|
||||||
setupClaudeCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Claude integration is installed")
|
setupClaudeCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Claude integration is installed")
|
||||||
setupClaudeCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd hooks from Claude settings")
|
setupClaudeCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd hooks from Claude settings")
|
||||||
|
setupClaudeCmd.Flags().BoolVar(&setupStealth, "stealth", false, "Use 'bd prime --stealth' (flush only, no git operations)")
|
||||||
|
|
||||||
setupCursorCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Cursor integration is installed")
|
setupCursorCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Cursor integration is installed")
|
||||||
setupCursorCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd rules from Cursor")
|
setupCursorCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd rules from Cursor")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// InstallClaude installs Claude Code hooks
|
// InstallClaude installs Claude Code hooks
|
||||||
func InstallClaude(project bool) {
|
func InstallClaude(project bool, stealth bool) {
|
||||||
var settingsPath string
|
var settingsPath string
|
||||||
|
|
||||||
if project {
|
if project {
|
||||||
@@ -49,13 +49,19 @@ func InstallClaude(project bool) {
|
|||||||
settings["hooks"] = hooks
|
settings["hooks"] = hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine which command to use
|
||||||
|
command := "bd prime"
|
||||||
|
if stealth {
|
||||||
|
command = "bd prime --stealth"
|
||||||
|
}
|
||||||
|
|
||||||
// Add SessionStart hook
|
// Add SessionStart hook
|
||||||
if addHookCommand(hooks, "SessionStart", "bd prime") {
|
if addHookCommand(hooks, "SessionStart", command) {
|
||||||
fmt.Println("✓ Registered SessionStart hook")
|
fmt.Println("✓ Registered SessionStart hook")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add PreCompact hook
|
// Add PreCompact hook
|
||||||
if addHookCommand(hooks, "PreCompact", "bd prime") {
|
if addHookCommand(hooks, "PreCompact", command) {
|
||||||
fmt.Println("✓ Registered PreCompact hook")
|
fmt.Println("✓ Registered PreCompact hook")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +143,11 @@ func RemoveClaude(project bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove bd prime hooks
|
// Remove bd prime hooks (both variants for backwards compatibility)
|
||||||
removeHookCommand(hooks, "SessionStart", "bd prime")
|
removeHookCommand(hooks, "SessionStart", "bd prime")
|
||||||
removeHookCommand(hooks, "PreCompact", "bd prime")
|
removeHookCommand(hooks, "PreCompact", "bd prime")
|
||||||
|
removeHookCommand(hooks, "SessionStart", "bd prime --stealth")
|
||||||
|
removeHookCommand(hooks, "PreCompact", "bd prime --stealth")
|
||||||
|
|
||||||
// Write back
|
// Write back
|
||||||
data, err = json.MarshalIndent(settings, "", " ")
|
data, err = json.MarshalIndent(settings, "", " ")
|
||||||
@@ -284,7 +292,9 @@ func hasBeadsHooks(settingsPath string) bool {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cmdMap["command"] == "bd prime" {
|
// Check for either variant
|
||||||
|
cmd := cmdMap["command"]
|
||||||
|
if cmd == "bd prime" || cmd == "bd prime --stealth" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ func TestAddHookCommand(t *testing.T) {
|
|||||||
command: "bd prime",
|
command: "bd prime",
|
||||||
wantAdded: true,
|
wantAdded: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "add stealth hook to empty hooks",
|
||||||
|
existingHooks: make(map[string]interface{}),
|
||||||
|
event: "SessionStart",
|
||||||
|
command: "bd prime --stealth",
|
||||||
|
wantAdded: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "hook already exists",
|
name: "hook already exists",
|
||||||
existingHooks: map[string]interface{}{
|
existingHooks: map[string]interface{}{
|
||||||
@@ -41,6 +48,25 @@ func TestAddHookCommand(t *testing.T) {
|
|||||||
command: "bd prime",
|
command: "bd prime",
|
||||||
wantAdded: false,
|
wantAdded: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "stealth hook already exists",
|
||||||
|
existingHooks: map[string]interface{}{
|
||||||
|
"SessionStart": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bd prime --stealth",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: "SessionStart",
|
||||||
|
command: "bd prime --stealth",
|
||||||
|
wantAdded: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "add second hook alongside existing",
|
name: "add second hook alongside existing",
|
||||||
existingHooks: map[string]interface{}{
|
existingHooks: map[string]interface{}{
|
||||||
@@ -122,6 +148,25 @@ func TestRemoveHookCommand(t *testing.T) {
|
|||||||
command: "bd prime",
|
command: "bd prime",
|
||||||
wantRemaining: 0,
|
wantRemaining: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "remove stealth hook",
|
||||||
|
existingHooks: map[string]interface{}{
|
||||||
|
"SessionStart": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bd prime --stealth",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: "SessionStart",
|
||||||
|
command: "bd prime --stealth",
|
||||||
|
wantRemaining: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "remove one of multiple hooks",
|
name: "remove one of multiple hooks",
|
||||||
existingHooks: map[string]interface{}{
|
existingHooks: map[string]interface{}{
|
||||||
@@ -184,9 +229,9 @@ func TestHasBeadsHooks(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
settingsData map[string]interface{}
|
settingsData map[string]interface{}
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "has bd prime hook",
|
name: "has bd prime hook",
|
||||||
@@ -208,9 +253,66 @@ func TestHasBeadsHooks(t *testing.T) {
|
|||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no hooks",
|
name: "has bd prime --stealth hook",
|
||||||
|
settingsData: map[string]interface{}{
|
||||||
|
"hooks": map[string]interface{}{
|
||||||
|
"SessionStart": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bd prime --stealth",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has bd prime in PreCompact",
|
||||||
|
settingsData: map[string]interface{}{
|
||||||
|
"hooks": map[string]interface{}{
|
||||||
|
"PreCompact": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bd prime",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has bd prime --stealth in PreCompact",
|
||||||
|
settingsData: map[string]interface{}{
|
||||||
|
"hooks": map[string]interface{}{
|
||||||
|
"PreCompact": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bd prime --stealth",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no hooks",
|
||||||
settingsData: map[string]interface{}{},
|
settingsData: map[string]interface{}{},
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "has other hooks but not bd prime",
|
name: "has other hooks but not bd prime",
|
||||||
@@ -242,7 +344,7 @@ func TestHasBeadsHooks(t *testing.T) {
|
|||||||
t.Fatalf("Failed to marshal test data: %v", err)
|
t.Fatalf("Failed to marshal test data: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
|
if err := os.WriteFile(settingsPath, data, 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,3 +378,31 @@ func TestIdempotency(t *testing.T) {
|
|||||||
t.Errorf("Expected 1 hook, got %d", len(eventHooks))
|
t.Errorf("Expected 1 hook, got %d", len(eventHooks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that running addHookCommand twice with stealth doesn't duplicate hooks
|
||||||
|
func TestIdempotencyWithStealth(t *testing.T) {
|
||||||
|
hooks := make(map[string]any)
|
||||||
|
|
||||||
|
if !addHookCommand(hooks, "SessionStart", "bd prime --stealth") {
|
||||||
|
t.Error("First call should have added the stealth hook")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second add (should detect existing)
|
||||||
|
if addHookCommand(hooks, "SessionStart", "bd prime --stealth") {
|
||||||
|
t.Error("Second call should have detected existing stealth hook")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify only one hook exists
|
||||||
|
eventHooks := hooks["SessionStart"].([]any)
|
||||||
|
if len(eventHooks) != 1 {
|
||||||
|
t.Errorf("Expected 1 hook, got %d", len(eventHooks))
|
||||||
|
}
|
||||||
|
|
||||||
|
// and that it's the correct one
|
||||||
|
hookMap := eventHooks[0].(map[string]any)
|
||||||
|
commands := hookMap["hooks"].([]any)
|
||||||
|
cmdMap := commands[0].(map[string]any)
|
||||||
|
if cmdMap["command"] != "bd prime --stealth" {
|
||||||
|
t.Errorf("Expected 'bd prime --stealth', got %v", cmdMap["command"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user