feat: support plugin-managed hooks
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
type HookStatusEntry,
|
||||
type HookStatusReport,
|
||||
} from "../hooks/hooks-status.js";
|
||||
import type { HookEntry } from "../hooks/types.js";
|
||||
import { loadWorkspaceHookEntries } from "../hooks/workspace.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/io.js";
|
||||
import {
|
||||
installHooksFromNpmSpec,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
resolveHookInstallDir,
|
||||
} from "../hooks/install.js";
|
||||
import { recordHookInstall } from "../hooks/installs.js";
|
||||
import { buildPluginStatusReport } from "../plugins/status.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
@@ -42,6 +45,29 @@ export type HooksUpdateOptions = {
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
function mergeHookEntries(
|
||||
pluginEntries: HookEntry[],
|
||||
workspaceEntries: HookEntry[],
|
||||
): HookEntry[] {
|
||||
const merged = new Map<string, HookEntry>();
|
||||
for (const entry of pluginEntries) {
|
||||
merged.set(entry.hook.name, entry);
|
||||
}
|
||||
for (const entry of workspaceEntries) {
|
||||
merged.set(entry.hook.name, entry);
|
||||
}
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
|
||||
function buildHooksReport(config: ClawdbotConfig): HookStatusReport {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config });
|
||||
const pluginReport = buildPluginStatusReport({ config, workspaceDir });
|
||||
const pluginEntries = pluginReport.hooks.map((hook) => hook.entry);
|
||||
const entries = mergeHookEntries(pluginEntries, workspaceEntries);
|
||||
return buildWorkspaceHookStatus(workspaceDir, { config, entries });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single hook for display in the list
|
||||
*/
|
||||
@@ -58,6 +84,9 @@ function formatHookLine(hook: HookStatusEntry, verbose = false): string {
|
||||
const desc = chalk.gray(
|
||||
hook.description.length > 50 ? `${hook.description.slice(0, 47)}...` : hook.description,
|
||||
);
|
||||
const sourceLabel = hook.managedByPlugin
|
||||
? chalk.magenta(`plugin:${hook.pluginId ?? "unknown"}`)
|
||||
: "";
|
||||
|
||||
if (verbose) {
|
||||
const missing: string[] = [];
|
||||
@@ -77,10 +106,12 @@ function formatHookLine(hook: HookStatusEntry, verbose = false): string {
|
||||
missing.push(`os: ${hook.missing.os.join(", ")}`);
|
||||
}
|
||||
const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : "";
|
||||
return `${emoji} ${name} ${status}${missingStr}\n ${desc}`;
|
||||
const sourceSuffix = sourceLabel ? ` ${sourceLabel}` : "";
|
||||
return `${emoji} ${name} ${status}${missingStr}\n ${desc}${sourceSuffix}`;
|
||||
}
|
||||
|
||||
return `${emoji} ${name} ${status} - ${desc}`;
|
||||
const sourceSuffix = sourceLabel ? ` ${sourceLabel}` : "";
|
||||
return `${emoji} ${name} ${status} - ${desc}${sourceSuffix}`;
|
||||
}
|
||||
|
||||
async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
@@ -110,9 +141,11 @@ export function formatHooksList(report: HookStatusReport, opts: HooksListOptions
|
||||
eligible: h.eligible,
|
||||
disabled: h.disabled,
|
||||
source: h.source,
|
||||
pluginId: h.pluginId,
|
||||
events: h.events,
|
||||
homepage: h.homepage,
|
||||
missing: h.missing,
|
||||
managedByPlugin: h.managedByPlugin,
|
||||
})),
|
||||
};
|
||||
return JSON.stringify(jsonReport, null, 2);
|
||||
@@ -186,7 +219,11 @@ export function formatHookInfo(
|
||||
|
||||
// Details
|
||||
lines.push(chalk.bold("Details:"));
|
||||
lines.push(` Source: ${hook.source}`);
|
||||
if (hook.managedByPlugin) {
|
||||
lines.push(` Source: ${hook.source} (${hook.pluginId ?? "unknown"})`);
|
||||
} else {
|
||||
lines.push(` Source: ${hook.source}`);
|
||||
}
|
||||
lines.push(` Path: ${chalk.gray(hook.filePath)}`);
|
||||
lines.push(` Handler: ${chalk.gray(hook.handlerPath)}`);
|
||||
if (hook.homepage) {
|
||||
@@ -195,6 +232,9 @@ export function formatHookInfo(
|
||||
if (hook.events.length > 0) {
|
||||
lines.push(` Events: ${hook.events.join(", ")}`);
|
||||
}
|
||||
if (hook.managedByPlugin) {
|
||||
lines.push(` Managed by plugin; enable/disable via hooks CLI not available.`);
|
||||
}
|
||||
|
||||
// Requirements
|
||||
const hasRequirements =
|
||||
@@ -302,14 +342,19 @@ export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptio
|
||||
|
||||
export async function enableHook(hookName: string): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
||||
const report = buildHooksReport(config);
|
||||
const hook = report.hooks.find((h) => h.name === hookName);
|
||||
|
||||
if (!hook) {
|
||||
throw new Error(`Hook "${hookName}" not found`);
|
||||
}
|
||||
|
||||
if (hook.managedByPlugin) {
|
||||
throw new Error(
|
||||
`Hook "${hookName}" is managed by plugin "${hook.pluginId ?? "unknown"}" and cannot be enabled/disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!hook.eligible) {
|
||||
throw new Error(`Hook "${hookName}" is not eligible (missing requirements)`);
|
||||
}
|
||||
@@ -336,14 +381,19 @@ export async function enableHook(hookName: string): Promise<void> {
|
||||
|
||||
export async function disableHook(hookName: string): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
||||
const report = buildHooksReport(config);
|
||||
const hook = report.hooks.find((h) => h.name === hookName);
|
||||
|
||||
if (!hook) {
|
||||
throw new Error(`Hook "${hookName}" not found`);
|
||||
}
|
||||
|
||||
if (hook.managedByPlugin) {
|
||||
throw new Error(
|
||||
`Hook "${hookName}" is managed by plugin "${hook.pluginId ?? "unknown"}" and cannot be enabled/disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Update config
|
||||
const entries = { ...config.hooks?.internal?.entries };
|
||||
entries[hookName] = { ...entries[hookName], enabled: false };
|
||||
@@ -382,8 +432,7 @@ export function registerHooksCli(program: Command): void {
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
||||
const report = buildHooksReport(config);
|
||||
console.log(formatHooksList(report, opts));
|
||||
} catch (err) {
|
||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||
@@ -398,8 +447,7 @@ export function registerHooksCli(program: Command): void {
|
||||
.action(async (name, opts) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
||||
const report = buildHooksReport(config);
|
||||
console.log(formatHookInfo(report, name, opts));
|
||||
} catch (err) {
|
||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||
@@ -414,8 +462,7 @@ export function registerHooksCli(program: Command): void {
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
||||
const report = buildHooksReport(config);
|
||||
console.log(formatHooksCheck(report, opts));
|
||||
} catch (err) {
|
||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||
@@ -765,8 +812,7 @@ export function registerHooksCli(program: Command): void {
|
||||
hooks.action(async () => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const report = buildWorkspaceHookStatus(workspaceDir, { config });
|
||||
const report = buildHooksReport(config);
|
||||
console.log(formatHooksList(report, {}));
|
||||
} catch (err) {
|
||||
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
||||
|
||||
Reference in New Issue
Block a user