import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; import { evaluateEntryRequirementsForCurrentPlatform } from "../shared/entry-status.js"; import type { RequirementConfigCheck, Requirements } from "../shared/requirements.js"; import { CONFIG_DIR } from "../utils.js"; import { hasBinary, isConfigPathTruthy, resolveHookConfig } from "./config.js"; import type { HookEligibilityContext, HookEntry, HookInstallSpec } from "./types.js"; import { loadWorkspaceHookEntries } from "./workspace.js"; export type HookStatusConfigCheck = RequirementConfigCheck; export type HookInstallOption = { id: string; kind: HookInstallSpec["kind"]; label: string; bins: string[]; }; export type HookStatusEntry = { name: string; description: string; source: string; pluginId?: string; filePath: string; baseDir: string; handlerPath: string; hookKey: string; emoji?: string; homepage?: string; events: string[]; always: boolean; disabled: boolean; eligible: boolean; managedByPlugin: boolean; requirements: Requirements; missing: Requirements; configChecks: HookStatusConfigCheck[]; install: HookInstallOption[]; }; export type HookStatusReport = { workspaceDir: string; managedHooksDir: string; hooks: HookStatusEntry[]; }; function resolveHookKey(entry: HookEntry): string { return entry.metadata?.hookKey ?? entry.hook.name; } function normalizeInstallOptions(entry: HookEntry): HookInstallOption[] { const install = entry.metadata?.install ?? []; if (install.length === 0) { return []; } // For hooks, we just list all install options return install.map((spec, index) => { const id = (spec.id ?? `${spec.kind}-${index}`).trim(); const bins = spec.bins ?? []; let label = (spec.label ?? "").trim(); if (!label) { if (spec.kind === "bundled") { label = "Bundled with OpenClaw"; } else if (spec.kind === "npm" && spec.package) { label = `Install ${spec.package} (npm)`; } else if (spec.kind === "git" && spec.repository) { label = `Install from ${spec.repository}`; } else { label = "Run installer"; } } return { id, kind: spec.kind, label, bins }; }); } function buildHookStatus( entry: HookEntry, config?: OpenClawConfig, eligibility?: HookEligibilityContext, ): HookStatusEntry { const hookKey = resolveHookKey(entry); const hookConfig = resolveHookConfig(config, hookKey); const managedByPlugin = entry.hook.source === "openclaw-plugin"; const disabled = managedByPlugin ? false : hookConfig?.enabled === false; const always = entry.metadata?.always === true; const events = entry.metadata?.events ?? []; const isEnvSatisfied = (envName: string) => Boolean(process.env[envName] || hookConfig?.env?.[envName]); const isConfigSatisfied = (pathStr: string) => isConfigPathTruthy(config, pathStr); const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } = evaluateEntryRequirementsForCurrentPlatform({ always, entry, hasLocalBin: hasBinary, remote: eligibility?.remote, isEnvSatisfied, isConfigSatisfied, }); const eligible = !disabled && requirementsSatisfied; return { name: entry.hook.name, description: entry.hook.description, source: entry.hook.source, pluginId: entry.hook.pluginId, filePath: entry.hook.filePath, baseDir: entry.hook.baseDir, handlerPath: entry.hook.handlerPath, hookKey, emoji, homepage, events, always, disabled, eligible, managedByPlugin, requirements: required, missing, configChecks, install: normalizeInstallOptions(entry), }; } export function buildWorkspaceHookStatus( workspaceDir: string, opts?: { config?: OpenClawConfig; managedHooksDir?: string; entries?: HookEntry[]; eligibility?: HookEligibilityContext; }, ): HookStatusReport { const managedHooksDir = opts?.managedHooksDir ?? path.join(CONFIG_DIR, "hooks"); const hookEntries = opts?.entries ?? loadWorkspaceHookEntries(workspaceDir, opts); return { workspaceDir, managedHooksDir, hooks: hookEntries.map((entry) => buildHookStatus(entry, opts?.config, opts?.eligibility)), }; }