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
130 changes: 25 additions & 105 deletions cmd/entire/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"

"entire.io/cli/cmd/entire/cli/agent"
"entire.io/cli/cmd/entire/cli/jsonutil"
Expand All @@ -30,14 +31,6 @@ type EntireSettings struct {
// Strategy is the name of the git strategy to use
Strategy string `json:"strategy"`

// Agent is the name of the coding agent (e.g., "claude-code", "cursor")
// When empty, auto-detection is used
Agent string `json:"agent,omitempty"`

// AgentAutoDetect controls whether to auto-detect the agent when not explicitly set
// Defaults to true for backwards compatibility
AgentAutoDetect *bool `json:"agent_auto_detect,omitempty"`

// Enabled indicates whether Entire is active. When false, CLI commands
// show a disabled message and hooks exit silently. Defaults to true.
Enabled bool `json:"enabled"`
Expand All @@ -54,10 +47,6 @@ type EntireSettings struct {
// StrategyOptions contains strategy-specific configuration
StrategyOptions map[string]interface{} `json:"strategy_options,omitempty"`

// AgentOptions contains agent-specific configuration
// Keyed by agent name, e.g., {"claude-code": {"ignore_untracked": false}}
AgentOptions map[string]interface{} `json:"agent_options,omitempty"`

// Telemetry controls anonymous usage analytics.
// nil = not asked yet (show prompt), true = opted in, false = opted out
Telemetry *bool `json:"telemetry,omitempty"`
Expand Down Expand Up @@ -166,41 +155,6 @@ func mergeSettingsJSON(settings *EntireSettings, data []byte) error {
}
}

// Override agent if present and non-empty
if agentRaw, ok := raw["agent"]; ok {
var a string
if err := json.Unmarshal(agentRaw, &a); err != nil {
return fmt.Errorf("parsing agent field: %w", err)
}
if a != "" {
settings.Agent = a
}
}

// Override agent_auto_detect if present
if autoDetectRaw, ok := raw["agent_auto_detect"]; ok {
var ad bool
if err := json.Unmarshal(autoDetectRaw, &ad); err != nil {
return fmt.Errorf("parsing agent_auto_detect field: %w", err)
}
settings.AgentAutoDetect = &ad
}

// Merge agent_options if present
if agentOptsRaw, ok := raw["agent_options"]; ok {
var opts map[string]interface{}
if err := json.Unmarshal(agentOptsRaw, &opts); err != nil {
return fmt.Errorf("parsing agent_options field: %w", err)
}
if settings.AgentOptions == nil {
settings.AgentOptions = opts
} else {
for k, v := range opts {
settings.AgentOptions[k] = v
}
}
}

// Override telemetry if present
if telemetryRaw, ok := raw["telemetry"]; ok {
var t bool
Expand Down Expand Up @@ -314,64 +268,6 @@ func GetStrategy() strategy.Strategy {
return s
}

// GetAgent returns the configured or detected agent.
// Resolution order:
// 1. Explicit agent in settings
// 2. Auto-detect if enabled (default)
// 3. Fall back to default agent
//

func GetAgent() (agent.Agent, error) {
settings, err := LoadEntireSettings()
if err != nil {
// No settings file, try auto-detect then default
if ag, detectErr := agent.Detect(); detectErr == nil {
return ag, nil
}
return agent.Default(), nil
}

// Explicit agent configured
if settings.Agent != "" {
ag, err := agent.Get(agent.AgentName(settings.Agent))
if err != nil {
return nil, fmt.Errorf("getting configured agent: %w", err)
}
return ag, nil
}

// Auto-detect if enabled (default true for backwards compat)
autoDetect := settings.AgentAutoDetect == nil || *settings.AgentAutoDetect
if autoDetect {
if ag, detectErr := agent.Detect(); detectErr == nil {
return ag, nil
}
}

// Fall back to default
return agent.Default(), nil
}

// GetAgentOptions returns options for a specific agent.
// Returns nil if the agent has no options configured.
func GetAgentOptions(agentName string) map[string]interface{} {
settings, err := LoadEntireSettings()
if err != nil {
return nil
}

if settings.AgentOptions == nil {
return nil
}

if opts, ok := settings.AgentOptions[agentName]; ok {
if m, ok := opts.(map[string]interface{}); ok {
return m
}
}
return nil
}

// GetLogLevel returns the configured log level from settings.
// Returns empty string if not configured (caller should use default).
// Note: ENTIRE_LOG_LEVEL env var takes precedence; check it first.
Expand All @@ -398,3 +294,27 @@ func IsMultiSessionWarningDisabled() bool {
}
return false
}

// GetAgentsWithHooksInstalled returns names of agents that have hooks installed.
func GetAgentsWithHooksInstalled() []agent.AgentName {
var installed []agent.AgentName
for _, name := range agent.List() {
ag, err := agent.Get(name)
if err != nil {
continue
}
if hs, ok := ag.(agent.HookSupport); ok && hs.AreHooksInstalled() {
installed = append(installed, name)
}
}
return installed
}

// JoinAgentNames joins agent names into a comma-separated string.
func JoinAgentNames(names []agent.AgentName) string {
strs := make([]string, len(names))
for i, n := range names {
strs[i] = string(n)
}
return strings.Join(strs, ",")
}
Comment on lines +298 to +320
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetAgentsWithHooksInstalled and JoinAgentNames are new helpers that are used to drive telemetry but currently have no direct tests in config_test.go, whereas other configuration helpers in this file are covered. Consider adding unit tests that exercise these functions (e.g., with a small set of registered test agents, with and without HookSupport) to ensure the reported agent list and joined string stay correct as agents evolve.

Copilot uses AI. Check for mistakes.
204 changes: 0 additions & 204 deletions cmd/entire/cli/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"path/filepath"
"testing"

"entire.io/cli/cmd/entire/cli/agent"
"entire.io/cli/cmd/entire/cli/strategy"
)

Expand Down Expand Up @@ -333,206 +332,3 @@ func TestLoadEntireSettings_NeitherFileExistsReturnsDefaults(t *testing.T) {
t.Error("Enabled should default to true")
}
}

func TestGetAgent_NoSettingsFile(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

// Create .claude directory to allow detection
if err := os.MkdirAll(".claude", 0o755); err != nil {
t.Fatalf("Failed to create .claude dir: %v", err)
}

ag, err := GetAgent()
if err != nil {
t.Fatalf("GetAgent() error = %v", err)
}
if ag == nil {
t.Fatal("GetAgent() returned nil agent")
}
// With .claude directory present, should detect Claude Code
if ag.Name() != agent.AgentNameClaudeCode {
t.Errorf("Expected claude-code agent, got %q", ag.Name())
}
}

func TestGetAgent_ExplicitAgent(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

settingsDir := filepath.Dir(EntireSettingsFile)
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
t.Fatalf("Failed to create settings dir: %v", err)
}

settingsContent := `{"strategy": "manual-commit", "agent": "claude-code"}`
if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil {
t.Fatalf("Failed to write settings file: %v", err)
}

ag, err := GetAgent()
if err != nil {
t.Fatalf("GetAgent() error = %v", err)
}
if ag.Name() != agent.AgentNameClaudeCode {
t.Errorf("Expected claude-code agent, got %q", ag.Name())
}
}

func TestGetAgent_AutoDetectDisabled(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

settingsDir := filepath.Dir(EntireSettingsFile)
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
t.Fatalf("Failed to create settings dir: %v", err)
}

// Create .claude directory but disable auto-detect
if err := os.MkdirAll(".claude", 0o755); err != nil {
t.Fatalf("Failed to create .claude dir: %v", err)
}

settingsContent := `{"strategy": "manual-commit", "agent_auto_detect": false}`
if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil {
t.Fatalf("Failed to write settings file: %v", err)
}

ag, err := GetAgent()
if err != nil {
t.Fatalf("GetAgent() error = %v", err)
}
// Should fall back to default when auto-detect is disabled and no explicit agent
if ag.Name() != agent.DefaultAgentName {
t.Errorf("Expected default agent %q, got %q", agent.DefaultAgentName, ag.Name())
}
}

func TestGetAgentOptions_ReturnsOptions(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

settingsDir := filepath.Dir(EntireSettingsFile)
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
t.Fatalf("Failed to create settings dir: %v", err)
}

settingsContent := `{
"strategy": "manual-commit",
"agent_options": {
"claude-code": {
"ignore_untracked": true
}
}
}`
if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil {
t.Fatalf("Failed to write settings file: %v", err)
}

opts := GetAgentOptions("claude-code")
if opts == nil {
t.Fatal("GetAgentOptions() returned nil")
}
if v, ok := opts["ignore_untracked"]; !ok || v != true {
t.Errorf("Expected ignore_untracked=true, got %v", v)
}
}

func TestGetAgentOptions_ReturnsNilForUnknownAgent(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

settingsDir := filepath.Dir(EntireSettingsFile)
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
t.Fatalf("Failed to create settings dir: %v", err)
}

settingsContent := `{
"strategy": "manual-commit",
"agent_options": {
"claude-code": {
"ignore_untracked": true
}
}
}`
if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil {
t.Fatalf("Failed to write settings file: %v", err)
}

opts := GetAgentOptions("unknown-agent")
if opts != nil {
t.Error("GetAgentOptions() should return nil for unknown agent")
}
}

func TestGetAgentOptions_ReturnsNilWhenNoSettings(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

opts := GetAgentOptions("claude-code")
if opts != nil {
t.Error("GetAgentOptions() should return nil when no settings file")
}
}

func TestLoadEntireSettings_AgentFields(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)

settingsDir := filepath.Dir(EntireSettingsFile)
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
t.Fatalf("Failed to create settings dir: %v", err)
}

settingsContent := `{
"strategy": "manual-commit",
"agent": "claude-code",
"agent_auto_detect": false,
"agent_options": {
"claude-code": {"option1": "value1"}
}
}`
if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil {
t.Fatalf("Failed to write settings file: %v", err)
}

settings, err := LoadEntireSettings()
if err != nil {
t.Fatalf("LoadEntireSettings() error = %v", err)
}

if settings.Agent != "claude-code" {
t.Errorf("Agent should be 'claude-code', got %q", settings.Agent)
}
if settings.AgentAutoDetect == nil || *settings.AgentAutoDetect {
t.Error("AgentAutoDetect should be false")
}
if settings.AgentOptions == nil {
t.Fatal("AgentOptions should not be nil")
}
if _, ok := settings.AgentOptions["claude-code"]; !ok {
t.Error("AgentOptions should have claude-code entry")
}
}

func TestLoadEntireSettings_LocalOverridesAgent(t *testing.T) {
setupLocalOverrideTestDir(t)

baseSettings := `{"strategy": "manual-commit", "agent": "cursor"}`
if err := os.WriteFile(EntireSettingsFile, []byte(baseSettings), 0o644); err != nil {
t.Fatalf("Failed to write settings file: %v", err)
}

localSettings := `{"agent": "claude-code"}`
if err := os.WriteFile(EntireSettingsLocalFile, []byte(localSettings), 0o644); err != nil {
t.Fatalf("Failed to write local settings file: %v", err)
}

settings, err := LoadEntireSettings()
if err != nil {
t.Fatalf("LoadEntireSettings() error = %v", err)
}
if settings.Agent != "claude-code" {
t.Errorf("Agent should be 'claude-code' from local override, got %q", settings.Agent)
}
}
6 changes: 1 addition & 5 deletions cmd/entire/cli/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,7 @@ func findTranscriptForSession(sessionID, repoRoot string) (string, error) {
return "", fmt.Errorf("failed to get agent for type %q: %w", sessionState.AgentType, err)
}
} else {
// Fall back to auto-detection if no session state
ag, err = GetAgent()
if err != nil {
return "", fmt.Errorf("failed to get agent: %w", err)
}
return "", fmt.Errorf("failed to get agent from sessionID: %s", sessionID)
}

// Get the session directory for this agent
Expand Down
Loading
Loading