@blundergoat/goat-flow
Version:
AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.
90 lines • 4.31 kB
JavaScript
/**
* Implements the `hooks` command family (list / sync / enable / disable) for the CLI.
* It is a thin presentation+validation layer over the server-side hook registrar: it lazy-imports
* the registrar so the heavy module only loads when a hooks command actually runs, picks JSON vs
* the compact text table from `--format`, and translates the registrar's typed errors into
* CLIErrors with the right exit code (404 -> usage error 2, everything else -> failure 1).
*/
import { CLIError } from "./cli-error.js";
import { writeOutput } from "./cli-output.js";
/** Render hook state as a compact terminal table. */
function renderHooksText(hooks) {
const lines = ["Hook state", ""];
for (const hook of hooks) {
const agents = hook.agents && typeof hook.agents === "object"
? hook.agents
: {};
const agentBits = Object.entries(agents).map(([agentId, state]) => {
if (state.supported === false)
return `${agentId}: not-supported`;
const installed = state.installed === true ? "installed" : "missing";
const drift = typeof state.drift === "string" ? ` (${state.drift})` : "";
return `${agentId}: ${installed}${drift}`;
});
lines.push(`${String(hook.id)} ${hook.enabled === true ? "enabled" : "disabled"} ${agentBits.join(", ")}`);
}
return lines.join("\n");
}
/**
* Assert a hook id is present for the enable/disable toggles, which cannot run without a target.
* Throws a usage CLIError (exit 2) naming the offending subcommand when the id is missing; the
* parser normally enforces this, so a throw here is a defensive guard for direct callers.
*/
function requireHookId(options) {
if (options.hookId)
return options.hookId;
throw new CLIError(`hooks ${options.hookSubcommand} requires <hook-id>.`, 2);
}
function renderHooksResult(options, result) {
writeOutput(options, options.format === "json"
? JSON.stringify(result, null, 2)
: renderHooksText(result.hooks));
}
/**
* Render the single hook returned by an enable/disable toggle, reusing the list table for one row.
* Emits JSON wrapping the hook under a `hook` key when `--format json`, otherwise the one-row text
* table, so toggle output stays shape-compatible with `hooks list` for scripts that parse either.
*/
function renderHookToggleResult(options, hook) {
writeOutput(options, options.format === "json"
? JSON.stringify({ hook }, null, 2)
: renderHooksText([hook]));
}
/**
* Handle the hooks command, dispatching list/sync/enable/disable to the lazily-imported registrar.
* Reports registrar failures as CLIErrors: a HookRegistrarError 404 (unknown hook) throws exit 2,
* any other registrar error throws exit 1, and non-registrar errors are rethrown unchanged. An
* unrecognised subcommand that reaches the end throws a usage CLIError (exit 2) with the syntax.
*
* @param options - parsed CLI options; reads `hookSubcommand`, `hookId`, `projectPath`, and `format`
* @returns a promise that resolves once output is written; rejects (throws) on the error paths above
*/
export async function handleHooksCommand(options) {
const { applyHookState, HookRegistrarError, readAllHookStates, syncHookStates, } = await import("./server/hook-registrar.js");
try {
switch (options.hookSubcommand) {
case "list":
renderHooksResult(options, {
hooks: readAllHookStates(options.projectPath),
});
return;
case "sync":
renderHooksResult(options, {
hooks: syncHookStates(options.projectPath),
});
return;
case "enable":
case "disable":
renderHookToggleResult(options, applyHookState(requireHookId(options), options.hookSubcommand === "enable", options.projectPath));
return;
}
}
catch (err) {
if (err instanceof HookRegistrarError) {
throw new CLIError(err.message, err.statusCode === 404 ? 2 : 1);
}
throw err;
}
throw new CLIError("Usage: goat-flow hooks <list|sync|enable <hook-id>|disable <hook-id>> [path]", 2);
}
//# sourceMappingURL=hooks-command.js.map