diff --git a/agents/entire-agent-pi/internal/pi/hooks.go b/agents/entire-agent-pi/internal/pi/hooks.go index 187c2c1..02cf500 100644 --- a/agents/entire-agent-pi/internal/pi/hooks.go +++ b/agents/entire-agent-pi/internal/pi/hooks.go @@ -131,24 +131,31 @@ func (a *Agent) AreHooksInstalled() bool { func generateExtension() string { return `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { execFileSync } from "node:child_process"; +import { execFile } from "node:child_process"; export default function (pi: ExtensionAPI) { - function fireHook(hookName: string, data: Record) { - try { - const json = JSON.stringify(data); - execFileSync("entire", ["hooks", "pi", hookName], { - input: json, - timeout: 10000, - stdio: ["pipe", "pipe", "pipe"], - }); - } catch { - // best effort — don't block the agent - } + function fireHook(hookName: string, data: Record): Promise { + return new Promise((resolve) => { + try { + const child = execFile( + "entire", + ["hooks", "pi", hookName], + { + timeout: 10000, + windowsHide: true, + }, + () => resolve(), + ); + child.stdin?.end(JSON.stringify(data)); + } catch { + // best effort — don't block the agent + resolve(); + } + }); } pi.on("session_start", async (_event, ctx) => { - fireHook("session_start", { + await fireHook("session_start", { type: "session_start", cwd: ctx.cwd, session_file: ctx.sessionManager.getSessionFile(), @@ -156,7 +163,7 @@ export default function (pi: ExtensionAPI) { }); pi.on("before_agent_start", async (event, ctx) => { - fireHook("before_agent_start", { + await fireHook("before_agent_start", { type: "before_agent_start", cwd: ctx.cwd, session_file: ctx.sessionManager.getSessionFile(), @@ -165,7 +172,7 @@ export default function (pi: ExtensionAPI) { }); pi.on("agent_end", async (_event, ctx) => { - fireHook("agent_end", { + await fireHook("agent_end", { type: "agent_end", cwd: ctx.cwd, session_file: ctx.sessionManager.getSessionFile(), @@ -173,7 +180,7 @@ export default function (pi: ExtensionAPI) { }); pi.on("session_shutdown", async () => { - fireHook("session_shutdown", { + await fireHook("session_shutdown", { type: "session_shutdown", }); }); diff --git a/agents/entire-agent-pi/internal/pi/hooks_test.go b/agents/entire-agent-pi/internal/pi/hooks_test.go index 3f3b435..24414d5 100644 --- a/agents/entire-agent-pi/internal/pi/hooks_test.go +++ b/agents/entire-agent-pi/internal/pi/hooks_test.go @@ -129,6 +129,69 @@ func TestParseHook_UnknownHook(t *testing.T) { } } +func TestGenerateExtension(t *testing.T) { + const want = `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { execFile } from "node:child_process"; + +export default function (pi: ExtensionAPI) { + function fireHook(hookName: string, data: Record): Promise { + return new Promise((resolve) => { + try { + const child = execFile( + "entire", + ["hooks", "pi", hookName], + { + timeout: 10000, + windowsHide: true, + }, + () => resolve(), + ); + child.stdin?.end(JSON.stringify(data)); + } catch { + // best effort — don't block the agent + resolve(); + } + }); + } + + pi.on("session_start", async (_event, ctx) => { + await fireHook("session_start", { + type: "session_start", + cwd: ctx.cwd, + session_file: ctx.sessionManager.getSessionFile(), + }); + }); + + pi.on("before_agent_start", async (event, ctx) => { + await fireHook("before_agent_start", { + type: "before_agent_start", + cwd: ctx.cwd, + session_file: ctx.sessionManager.getSessionFile(), + prompt: event.prompt, + }); + }); + + pi.on("agent_end", async (_event, ctx) => { + await fireHook("agent_end", { + type: "agent_end", + cwd: ctx.cwd, + session_file: ctx.sessionManager.getSessionFile(), + }); + }); + + pi.on("session_shutdown", async () => { + await fireHook("session_shutdown", { + type: "session_shutdown", + }); + }); +} +` + + if got := generateExtension(); got != want { + t.Fatalf("generateExtension() mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want) + } +} + func TestInstallAndUninstallHooks(t *testing.T) { tmp := t.TempDir() t.Setenv("ENTIRE_REPO_ROOT", tmp) @@ -151,14 +214,13 @@ func TestInstallAndUninstallHooks(t *testing.T) { t.Error("hooks should be installed after InstallHooks") } - // Verify the extension file exists and contains expected content. extPath := filepath.Join(tmp, extensionFile) data, err := os.ReadFile(extPath) if err != nil { t.Fatal(err) } - if len(data) == 0 { - t.Error("extension file is empty") + if string(data) != generateExtension() { + t.Fatal("installed extension does not match generated extension") } // Idempotent install should return 0.