Skip to content
Open
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
22 changes: 11 additions & 11 deletions cmd/entire/cli/agent/opencode/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm assuming this is an optimization to avoid having to overwrite the same file over and over again, correct?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

As it was before we did not update it at all but just checked if it's there so I went with this. But I guess on the other side since this happens only in the context of entire enable we could also just always write it? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm fine, either way. I was mostly curious about the reason to validate the content.

return 0, nil // Already up-to-date
}
Comment on lines +65 to +67
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The InstallHooks doc comment says it returns 0 when the plugin is "already present". With the new content-equality idempotency check, a file can be present but still be rewritten (returning 1). Consider updating the wording to "0 if already up-to-date" (and/or mention that differing content will be rewritten unless --force is false).

Copilot uses AI. Check for mistakes.
}
Comment on lines +62 to +68
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

New behavior: when the plugin file exists but its content differs (e.g., switching localDev/prod or template updates), InstallHooks now rewrites it. There isn’t a test covering this update path (only fresh/idempotent/force/localDev). Add a unit test that pre-writes a different entire.ts (or installs once with localDev=true then installs with localDev=false) and asserts InstallHooks returns 1 and rewrites the file when force=false.

Copilot generated this review using guidance from repository custom instructions.
}

// Ensure directory exists
pluginDir := filepath.Dir(pluginPath)
//nolint:gosec // G301: Plugin directory needs standard permissions
Expand Down
44 changes: 44 additions & 0 deletions cmd/entire/cli/agent/opencode/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading