Skip to content

Commit 76a29cc

Browse files
authored
Merge pull request #7 from entireio/pi-hooks-async-execfile
pi: await entire hook completion in the extension
2 parents 0509431 + 48c9515 commit 76a29cc

File tree

2 files changed

+88
-19
lines changed

2 files changed

+88
-19
lines changed

agents/entire-agent-pi/internal/pi/hooks.go

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -131,32 +131,39 @@ func (a *Agent) AreHooksInstalled() bool {
131131

132132
func generateExtension() string {
133133
return `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
134-
import { execFileSync } from "node:child_process";
134+
import { execFile } from "node:child_process";
135135
136136
export default function (pi: ExtensionAPI) {
137-
function fireHook(hookName: string, data: Record<string, unknown>) {
138-
try {
139-
const json = JSON.stringify(data);
140-
execFileSync("entire", ["hooks", "pi", hookName], {
141-
input: json,
142-
timeout: 10000,
143-
stdio: ["pipe", "pipe", "pipe"],
144-
});
145-
} catch {
146-
// best effort — don't block the agent
147-
}
137+
function fireHook(hookName: string, data: Record<string, unknown>): Promise<void> {
138+
return new Promise((resolve) => {
139+
try {
140+
const child = execFile(
141+
"entire",
142+
["hooks", "pi", hookName],
143+
{
144+
timeout: 10000,
145+
windowsHide: true,
146+
},
147+
() => resolve(),
148+
);
149+
child.stdin?.end(JSON.stringify(data));
150+
} catch {
151+
// best effort — don't block the agent
152+
resolve();
153+
}
154+
});
148155
}
149156
150157
pi.on("session_start", async (_event, ctx) => {
151-
fireHook("session_start", {
158+
await fireHook("session_start", {
152159
type: "session_start",
153160
cwd: ctx.cwd,
154161
session_file: ctx.sessionManager.getSessionFile(),
155162
});
156163
});
157164
158165
pi.on("before_agent_start", async (event, ctx) => {
159-
fireHook("before_agent_start", {
166+
await fireHook("before_agent_start", {
160167
type: "before_agent_start",
161168
cwd: ctx.cwd,
162169
session_file: ctx.sessionManager.getSessionFile(),
@@ -165,15 +172,15 @@ export default function (pi: ExtensionAPI) {
165172
});
166173
167174
pi.on("agent_end", async (_event, ctx) => {
168-
fireHook("agent_end", {
175+
await fireHook("agent_end", {
169176
type: "agent_end",
170177
cwd: ctx.cwd,
171178
session_file: ctx.sessionManager.getSessionFile(),
172179
});
173180
});
174181
175182
pi.on("session_shutdown", async () => {
176-
fireHook("session_shutdown", {
183+
await fireHook("session_shutdown", {
177184
type: "session_shutdown",
178185
});
179186
});

agents/entire-agent-pi/internal/pi/hooks_test.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,69 @@ func TestParseHook_UnknownHook(t *testing.T) {
129129
}
130130
}
131131

132+
func TestGenerateExtension(t *testing.T) {
133+
const want = `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
134+
import { execFile } from "node:child_process";
135+
136+
export default function (pi: ExtensionAPI) {
137+
function fireHook(hookName: string, data: Record<string, unknown>): Promise<void> {
138+
return new Promise((resolve) => {
139+
try {
140+
const child = execFile(
141+
"entire",
142+
["hooks", "pi", hookName],
143+
{
144+
timeout: 10000,
145+
windowsHide: true,
146+
},
147+
() => resolve(),
148+
);
149+
child.stdin?.end(JSON.stringify(data));
150+
} catch {
151+
// best effort — don't block the agent
152+
resolve();
153+
}
154+
});
155+
}
156+
157+
pi.on("session_start", async (_event, ctx) => {
158+
await fireHook("session_start", {
159+
type: "session_start",
160+
cwd: ctx.cwd,
161+
session_file: ctx.sessionManager.getSessionFile(),
162+
});
163+
});
164+
165+
pi.on("before_agent_start", async (event, ctx) => {
166+
await fireHook("before_agent_start", {
167+
type: "before_agent_start",
168+
cwd: ctx.cwd,
169+
session_file: ctx.sessionManager.getSessionFile(),
170+
prompt: event.prompt,
171+
});
172+
});
173+
174+
pi.on("agent_end", async (_event, ctx) => {
175+
await fireHook("agent_end", {
176+
type: "agent_end",
177+
cwd: ctx.cwd,
178+
session_file: ctx.sessionManager.getSessionFile(),
179+
});
180+
});
181+
182+
pi.on("session_shutdown", async () => {
183+
await fireHook("session_shutdown", {
184+
type: "session_shutdown",
185+
});
186+
});
187+
}
188+
`
189+
190+
if got := generateExtension(); got != want {
191+
t.Fatalf("generateExtension() mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want)
192+
}
193+
}
194+
132195
func TestInstallAndUninstallHooks(t *testing.T) {
133196
tmp := t.TempDir()
134197
t.Setenv("ENTIRE_REPO_ROOT", tmp)
@@ -151,14 +214,13 @@ func TestInstallAndUninstallHooks(t *testing.T) {
151214
t.Error("hooks should be installed after InstallHooks")
152215
}
153216

154-
// Verify the extension file exists and contains expected content.
155217
extPath := filepath.Join(tmp, extensionFile)
156218
data, err := os.ReadFile(extPath)
157219
if err != nil {
158220
t.Fatal(err)
159221
}
160-
if len(data) == 0 {
161-
t.Error("extension file is empty")
222+
if string(data) != generateExtension() {
223+
t.Fatal("installed extension does not match generated extension")
162224
}
163225

164226
// Idempotent install should return 0.

0 commit comments

Comments
 (0)