Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 115 additions & 71 deletions cmd/entire/cli/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/agent/external"
"github.com/entireio/cli/cmd/entire/cli/agent/types"
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/session"
"github.com/entireio/cli/cmd/entire/cli/settings"
Expand Down Expand Up @@ -185,9 +186,9 @@ func runSetupFlow(ctx context.Context, w io.Writer, opts EnableOptions) error {
return runEnableInteractive(ctx, w, agents, opts)
}

// runAddAgents shows which agents are currently enabled and lets the user add more.
// Already-installed agents cannot be deselected.
func runAddAgents(ctx context.Context, w io.Writer, opts EnableOptions) error {
// runManageAgents shows which agents are currently enabled and lets the user
// add or remove agents. Deselecting an installed agent removes its hooks.
func runManageAgents(ctx context.Context, w io.Writer, opts EnableOptions, selectFn func(available []string) ([]string, error)) error {
installedNames := GetAgentsWithHooksInstalled(ctx)

// Show currently installed agents
Expand Down Expand Up @@ -245,90 +246,119 @@ func runAddAgents(ctx context.Context, w io.Writer, opts EnableOptions) error {
return errors.New("no agents with hook support available")
}

// Check if all agents are already installed
allInstalled := true
for _, name := range agentNames {
ag, err := agent.Get(name)
// Collect available agent names for the selector
availableNames := make([]string, 0, len(options))
for _, opt := range options {
availableNames = append(availableNames, opt.Value)
}

var selectedAgentNames []string
if selectFn != nil {
var err error
selectedAgentNames, err = selectFn(availableNames)
if err != nil {
continue
}
if _, ok := agent.AsHookSupport(ag); !ok {
continue
return fmt.Errorf("agent selection cancelled: %w", err)
}
if to, ok := ag.(agent.TestOnly); ok && to.IsTestOnly() {
continue
}
if _, installed := installedSet[name]; !installed {
allInstalled = false
break
} else {
form := NewAccessibleForm(
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Manage agents").
Description("Use space to select/deselect, enter to confirm.").
Options(options...).
Value(&selectedAgentNames),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("agent selection cancelled: %w", err)
}
}
if allInstalled {
fmt.Fprintln(w, "All available agents are already enabled.")

// Nothing selected and nothing installed — no-op.
if len(selectedAgentNames) == 0 && len(installedNames) == 0 {
fmt.Fprintln(w, "No changes made.")
return nil
}

var selectedAgentNames []string
form := NewAccessibleForm(
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Enable more agents").
Description("Use space to select, enter to confirm.").
Options(options...).
Validate(func(selected []string) error {
if len(selected) == 0 {
return errors.New("please select at least one agent")
}
// Ensure previously installed agents are still selected
selectedSet := make(map[string]struct{}, len(selected))
for _, s := range selected {
selectedSet[s] = struct{}{}
}
for _, name := range installedNames {
if _, ok := selectedSet[string(name)]; !ok {
ag, err := agent.Get(name)
if err != nil {
continue
}
return fmt.Errorf("cannot remove %s — use `entire configure --remove %s` to remove it", ag.Type(), name)
}
}
return nil
}).
Value(&selectedAgentNames),
),
)
Comment thread
peyton-alt marked this conversation as resolved.
if err := form.Run(); err != nil {
return fmt.Errorf("agent selection cancelled: %w", err)
err := applyAgentChanges(ctx, w, selectedAgentNames, installedNames, opts)
if err == nil && len(selectedAgentNames) == 0 {
fmt.Fprintln(w, "To add agents again, run: entire configure --agent <name>")
}
return err
}

// applyAgentChanges computes added/removed agent sets from the selection and
// installs or uninstalls hooks accordingly.
func applyAgentChanges(ctx context.Context, w io.Writer, selectedAgentNames []string, installedNames []types.AgentName, opts EnableOptions) error {
installedSet := make(map[types.AgentName]struct{}, len(installedNames))
for _, name := range installedNames {
installedSet[name] = struct{}{}
}

selectedSet := make(map[string]struct{}, len(selectedAgentNames))
for _, name := range selectedAgentNames {
selectedSet[name] = struct{}{}
}

// Collect errors so partial successes are visible to the user.
var errs []error

// Find newly selected agents (not previously installed)
var newAgents []agent.Agent
var addedAgents []agent.Agent
for _, name := range selectedAgentNames {
if _, wasInstalled := installedSet[types.AgentName(name)]; wasInstalled {
continue
}
ag, err := agent.Get(types.AgentName(name))
if err != nil {
return fmt.Errorf("failed to get agent %s: %w", name, err)
errs = append(errs, fmt.Errorf("failed to get agent %s: %w", name, err))
continue
}
newAgents = append(newAgents, ag)
addedAgents = append(addedAgents, ag)
}

if len(newAgents) == 0 {
fmt.Fprintln(w, "No new agents selected.")
return nil
var removedAgents []agent.Agent
for _, name := range installedNames {
if _, stillSelected := selectedSet[string(name)]; stillSelected {
continue
}
ag, err := agent.Get(name)
if err != nil {
errs = append(errs, fmt.Errorf("failed to load deselected agent %s: %w", name, err))
continue
}
removedAgents = append(removedAgents, ag)
}

Comment thread
peyton-alt marked this conversation as resolved.
// Install hooks for newly selected agents only
for _, ag := range newAgents {
if len(addedAgents) == 0 && len(removedAgents) == 0 && len(errs) == 0 {
fmt.Fprintln(w, "No changes made.")
return nil
}
var installedAgents []agent.Agent
for _, ag := range addedAgents {
if _, err := setupAgentHooks(ctx, ag, opts.LocalDev, opts.ForceHooks); err != nil {
return fmt.Errorf("failed to setup %s hooks: %w", ag.Type(), err)
errs = append(errs, fmt.Errorf("failed to setup %s hooks: %w", ag.Type(), err))
} else {
installedAgents = append(installedAgents, ag)
}
}

var uninstalledAgents []agent.Agent
for _, ag := range removedAgents {
hookAgent, ok := agent.AsHookSupport(ag)
if !ok {
logging.Warn(ctx, "installed agent does not support hooks, skipping removal",
"agent", string(ag.Name()))
continue
}
if err := hookAgent.UninstallHooks(ctx); err != nil {
errs = append(errs, fmt.Errorf("failed to remove %s hooks: %w", ag.Type(), err))
} else {
uninstalledAgents = append(uninstalledAgents, ag)
}
}

// Auto-enable external_agents setting if any new agent is external.
for _, ag := range newAgents {
for _, ag := range installedAgents {
if external.IsExternal(ag) {
s, loadErr := LoadEntireSettings(ctx)
if loadErr != nil {
Expand All @@ -343,20 +373,34 @@ func runAddAgents(ctx context.Context, w io.Writer, opts EnableOptions) error {
saveErr = SaveEntireSettings(ctx, s)
}
if saveErr != nil {
return fmt.Errorf("failed to save external_agents setting: %w", saveErr)
errs = append(errs, fmt.Errorf("failed to save external_agents setting: %w", saveErr))
}
}
break
}
}

newTypes := make([]string, 0, len(newAgents))
for _, ag := range newAgents {
newTypes = append(newTypes, string(ag.Type()))
// Print summary of what succeeded
if len(installedAgents) > 0 {
names := make([]string, 0, len(installedAgents))
for _, ag := range installedAgents {
names = append(names, string(ag.Type()))
}
fmt.Fprintf(w, "✓ Added agents: %s\n", strings.Join(names, ", "))
}
if len(uninstalledAgents) > 0 {
if len(installedAgents) == 0 && len(addedAgents) == 0 && len(removedAgents) == len(installedNames) {
fmt.Fprintln(w, "All agents have been removed.")
} else {
names := make([]string, 0, len(uninstalledAgents))
for _, ag := range uninstalledAgents {
names = append(names, string(ag.Type()))
}
fmt.Fprintf(w, "✓ Removed agents: %s\n", strings.Join(names, ", "))
}
}
fmt.Fprintf(w, "✓ Added agents: %s\n", strings.Join(newTypes, ", "))

return nil
return errors.Join(errs...)
}

func newSetupCmd() *cobra.Command {
Expand All @@ -370,9 +414,9 @@ func newSetupCmd() *cobra.Command {
Long: `Configure Entire with session tracking for your AI agent workflows.

On first run, this configures Entire and installs agent hooks.
On subsequent runs, it lets you enable more agents.
On subsequent runs, it lets you add or remove agents interactively.

Use --remove to remove a specific agent's hooks:
Use --remove to remove a specific agent non-interactively:
entire configure --remove claude-code`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
Expand Down Expand Up @@ -413,7 +457,7 @@ Use --remove to remove a specific agent's hooks:

// If already set up, show agents and let user add more
if settings.IsSetUpAny(ctx) {
return runAddAgents(ctx, cmd.OutOrStdout(), opts)
return runManageAgents(ctx, cmd.OutOrStdout(), opts, nil)
}

// Fresh repo — run full setup flow
Expand Down
Loading
Loading