hook notifs
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import * as os from "node:os";
|
import { basename, dirname, join, resolve } from "node:path";
|
||||||
import { dirname, join, resolve } from "node:path";
|
|
||||||
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
const HOOK_TIMEOUT_MS = 10 * 60 * 1000;
|
const HOOK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
const LOG_FILE = join(os.homedir(), ".pi", "hooks-resolution.log");
|
|
||||||
|
|
||||||
type HookEventName = "PostToolUse" | "PostToolUseFailure";
|
type HookEventName = "PostToolUse" | "PostToolUseFailure";
|
||||||
|
|
||||||
@@ -30,18 +28,6 @@ type CommandRunResult = {
|
|||||||
timedOut: boolean;
|
timedOut: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function logHook(message: string): void {
|
|
||||||
const line = `[${new Date().toISOString()}] ${message}`;
|
|
||||||
console.error(`[hooks-resolution] ${line}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
||||||
appendFileSync(LOG_FILE, `${line}\n`);
|
|
||||||
} catch {
|
|
||||||
// ignore logging failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFile(path: string): boolean {
|
function isFile(path: string): boolean {
|
||||||
try {
|
try {
|
||||||
return statSync(path).isFile();
|
return statSync(path).isFile();
|
||||||
@@ -471,6 +457,23 @@ function hookEventNameForResult(event: ToolResultEvent): HookEventName {
|
|||||||
return event.isError ? "PostToolUseFailure" : "PostToolUse";
|
return event.isError ? "PostToolUseFailure" : "PostToolUse";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDuration(elapsedMs: number): string {
|
||||||
|
if (elapsedMs < 1000) {
|
||||||
|
return `${elapsedMs}ms`;
|
||||||
|
}
|
||||||
|
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookName(command: string): string {
|
||||||
|
const shPathMatch = command.match(/[^\s|;&]+\.sh\b/);
|
||||||
|
if (shPathMatch) {
|
||||||
|
return basename(shPathMatch[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstToken = command.trim().split(/\s+/)[0] ?? "hook";
|
||||||
|
return basename(firstToken);
|
||||||
|
}
|
||||||
|
|
||||||
export default function(pi: ExtensionAPI) {
|
export default function(pi: ExtensionAPI) {
|
||||||
let state: HookState = {
|
let state: HookState = {
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -479,13 +482,6 @@ export default function(pi: ExtensionAPI) {
|
|||||||
|
|
||||||
const refreshHooks = (cwd: string) => {
|
const refreshHooks = (cwd: string) => {
|
||||||
state = loadHooks(cwd);
|
state = loadHooks(cwd);
|
||||||
const postCount = state.hooks.filter((hook) => hook.eventName === "PostToolUse").length;
|
|
||||||
const failureCount = state.hooks.filter(
|
|
||||||
(hook) => hook.eventName === "PostToolUseFailure",
|
|
||||||
).length;
|
|
||||||
logHook(
|
|
||||||
`loaded hooks projectDir=${state.projectDir} postToolUse=${postCount} postToolUseFailure=${failureCount}`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pi.on("session_start", (_event, ctx) => {
|
pi.on("session_start", (_event, ctx) => {
|
||||||
@@ -514,31 +510,26 @@ export default function(pi: ExtensionAPI) {
|
|||||||
|
|
||||||
for (const hook of matchingHooks) {
|
for (const hook of matchingHooks) {
|
||||||
if (executedCommands.has(hook.command)) {
|
if (executedCommands.has(hook.command)) {
|
||||||
logHook(
|
|
||||||
`deduped event=${eventName} tool=${event.toolName} source=${hook.source} command=${JSON.stringify(hook.command)}`,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
executedCommands.add(hook.command);
|
executedCommands.add(hook.command);
|
||||||
|
|
||||||
logHook(
|
|
||||||
`run event=${eventName} tool=${event.toolName} matcher=${hook.matcherText ?? "*"} source=${hook.source} command=${JSON.stringify(hook.command)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await runCommandHook(hook.command, state.projectDir, payload);
|
const result = await runCommandHook(hook.command, state.projectDir, payload);
|
||||||
logHook(
|
const name = hookName(hook.command);
|
||||||
`done event=${eventName} tool=${event.toolName} code=${result.code} durationMs=${result.elapsedMs} timedOut=${result.timedOut}`,
|
const duration = formatDuration(result.elapsedMs);
|
||||||
);
|
|
||||||
|
if (result.code === 0) {
|
||||||
|
ctx.ui.notify(` Hook \`${name}\` executed, took ${duration}`, "info");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.code !== 0) {
|
|
||||||
const matcherLabel = hook.matcherText ?? "*";
|
const matcherLabel = hook.matcherText ?? "*";
|
||||||
const errorLine =
|
const errorLine =
|
||||||
result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
`Hook failed (${matcherLabel}) from ${hook.source}: ${errorLine}`,
|
` Hook \`${name}\` failed after ${duration} (${matcherLabel}) from ${hook.source}: ${errorLine}`,
|
||||||
"warning",
|
"warning",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user