diff --git a/cmd/entire/cli/agent/opencode/hooks.go b/cmd/entire/cli/agent/opencode/hooks.go index 35b8ed459..02b2f7cb7 100644 --- a/cmd/entire/cli/agent/opencode/hooks.go +++ b/cmd/entire/cli/agent/opencode/hooks.go @@ -40,23 +40,14 @@ func getPluginPath(ctx context.Context) (string, error) { } // InstallHooks writes the Entire plugin file to .opencode/plugins/entire.ts. -// Returns 1 if the plugin was installed, 0 if already present (idempotent). +// Returns 1 if the plugin was written, 0 if already up-to-date (idempotent). +// If the file exists but content differs (e.g., localDev vs production), it is rewritten. func (a *OpenCodeAgent) InstallHooks(ctx context.Context, localDev bool, force bool) (int, error) { pluginPath, err := getPluginPath(ctx) if err != nil { return 0, err } - // Check if already installed (idempotent) unless force - if !force { - if _, err := os.Stat(pluginPath); err == nil { - data, readErr := os.ReadFile(pluginPath) //nolint:gosec // Path constructed from repo root - if readErr == nil && strings.Contains(string(data), entireMarker) { - return 0, nil // Already installed - } - } - } - // Build the command prefix var cmdPrefix string if localDev { @@ -68,6 +59,15 @@ func (a *OpenCodeAgent) InstallHooks(ctx context.Context, localDev bool, force b // Generate plugin content from template content := strings.ReplaceAll(pluginTemplate, entireCmdPlaceholder, cmdPrefix) + // Check if already installed with identical content (idempotent) unless force + if !force { + if existing, readErr := os.ReadFile(pluginPath); readErr == nil { //nolint:gosec // Path constructed from repo root + if string(existing) == content { + return 0, nil // Already up-to-date + } + } + } + // Ensure directory exists pluginDir := filepath.Dir(pluginPath) //nolint:gosec // G301: Plugin directory needs standard permissions diff --git a/cmd/entire/cli/agent/opencode/hooks_test.go b/cmd/entire/cli/agent/opencode/hooks_test.go index 744714b26..a5c717cb5 100644 --- a/cmd/entire/cli/agent/opencode/hooks_test.go +++ b/cmd/entire/cli/agent/opencode/hooks_test.go @@ -121,6 +121,50 @@ func TestInstallHooks_ForceReinstall(t *testing.T) { } } +func TestInstallHooks_RewritesWhenContentDiffers(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + ag := &OpenCodeAgent{} + + // Install with localDev=true + count, err := ag.InstallHooks(context.Background(), true, false) + if err != nil { + t.Fatalf("first install failed: %v", err) + } + if count != 1 { + t.Errorf("first install: expected 1, got %d", count) + } + + pluginPath := filepath.Join(dir, ".opencode", "plugins", "entire.ts") + before, err := os.ReadFile(pluginPath) + if err != nil { + t.Fatalf("failed to read plugin file: %v", err) + } + if !strings.Contains(string(before), "go run") { + t.Fatal("expected localDev content with 'go run'") + } + + // Reinstall with localDev=false (content differs) — should rewrite + count, err = ag.InstallHooks(context.Background(), false, false) + if err != nil { + t.Fatalf("second install failed: %v", err) + } + if count != 1 { + t.Errorf("second install with different content: expected 1, got %d", count) + } + + after, err := os.ReadFile(pluginPath) + if err != nil { + t.Fatalf("failed to read plugin file after rewrite: %v", err) + } + if strings.Contains(string(after), "go run") { + t.Error("expected production content after rewrite, but still contains 'go run'") + } + if !strings.Contains(string(after), `const ENTIRE_CMD = 'entire'`) { + t.Error("expected production command constant after rewrite") + } +} + func TestUninstallHooks(t *testing.T) { dir := t.TempDir() t.Chdir(dir)