85 lines
2.9 KiB
JavaScript
85 lines
2.9 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { exists, isRecord } from "./utils.mjs";
|
|
|
|
const EVENT_LABELS = new Map([
|
|
["PreToolUse", "pre_tool_use"],
|
|
["PermissionRequest", "permission_request"],
|
|
["PostToolUse", "post_tool_use"],
|
|
["PreCompact", "pre_compact"],
|
|
["PostCompact", "post_compact"],
|
|
["SessionStart", "session_start"],
|
|
["UserPromptSubmit", "user_prompt_submit"],
|
|
["SubagentStart", "subagent_start"],
|
|
["SubagentStop", "subagent_stop"],
|
|
["Stop", "stop"],
|
|
]);
|
|
|
|
export async function trustedHookStatesForPlugin({ marketplaceName, pluginName, pluginRoot }) {
|
|
const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
|
|
if (!(await exists(manifestPath))) return [];
|
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
if (!isRecord(manifest) || typeof manifest.hooks !== "string") return [];
|
|
|
|
const hooksPath = join(pluginRoot, manifest.hooks);
|
|
if (!(await exists(hooksPath))) return [];
|
|
const parsed = JSON.parse(await readFile(hooksPath, "utf8"));
|
|
if (!isRecord(parsed) || !isRecord(parsed.hooks)) return [];
|
|
|
|
const keySource = `${pluginName}@${marketplaceName}:${stripDotSlash(manifest.hooks)}`;
|
|
const states = [];
|
|
for (const [eventName, groups] of Object.entries(parsed.hooks)) {
|
|
if (!Array.isArray(groups)) continue;
|
|
const eventLabel = EVENT_LABELS.get(eventName);
|
|
if (eventLabel === undefined) continue;
|
|
for (const [groupIndex, group] of groups.entries()) {
|
|
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
for (const [handlerIndex, handler] of group.hooks.entries()) {
|
|
if (!isRecord(handler) || handler.type !== "command") continue;
|
|
if (handler.async === true) continue;
|
|
if (typeof handler.command !== "string" || handler.command.trim() === "") continue;
|
|
const key = `${keySource}:${eventLabel}:${groupIndex}:${handlerIndex}`;
|
|
states.push({
|
|
key,
|
|
trustedHash: commandHookHash(eventLabel, group.matcher, handler),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return states;
|
|
}
|
|
|
|
function commandHookHash(eventName, matcher, handler) {
|
|
const command = handler.command;
|
|
const timeout = Math.max(Number(handler.timeout ?? 600), 1);
|
|
const normalizedHandler = {
|
|
type: "command",
|
|
command,
|
|
timeout,
|
|
async: false,
|
|
};
|
|
if (typeof handler.statusMessage === "string") normalizedHandler.statusMessage = handler.statusMessage;
|
|
const identity = {
|
|
event_name: eventName,
|
|
hooks: [normalizedHandler],
|
|
};
|
|
if (typeof matcher === "string") identity.matcher = matcher;
|
|
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalJson(identity))).digest("hex")}`;
|
|
}
|
|
|
|
function canonicalJson(value) {
|
|
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
if (!isRecord(value)) return value;
|
|
const result = {};
|
|
for (const key of Object.keys(value).sort()) {
|
|
result[key] = canonicalJson(value[key]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function stripDotSlash(value) {
|
|
return value.startsWith("./") ? value.slice(2) : value;
|
|
}
|