Files
jackwener__opencli/src/hooks.ts
T
jakevin 15c6d0d508 refactor: deduplicate code, improve type safety, simplify error classes (#480)
- Extract shared parseYamlArgs() to yaml-schema.ts, eliminating duplicate
  YAML args parsing in discovery.ts and build-manifest.ts
- Unify BROWSER_ONLY_STEPS: export from capabilityRouting.ts, reuse in
  pipeline executor (fixes missing intercept/tap in retry set)
- Remove dead normalizeArgValue from commanderAdapter; bool coercion now
  handled solely by coerceAndValidateArgs in execution.ts
- Add closeWindow?() to IPage interface, replacing unsafe casts in executor
- BrowserBridge/CDPBridge implement IBrowserFactory, removing double cast
  in getBrowserFactory()
- Simplify CliError subclasses with new.target.name (9 redundant this.name
  assignments removed)
- Add hook dedup in addHook() to prevent duplicate registrations
- Fix normalizeRows to safely handle primitive values
- Unify CommandArgs type: execution.ts now imports from registry.ts
- Cache strategyLabel() call in cli.ts list command
2026-03-27 02:45:42 +08:00

92 lines
3.1 KiB
TypeScript

/**
* Plugin lifecycle hooks: allows plugins to tap into opencli's execution lifecycle.
*
* Hooks use globalThis (like the command registry) to guarantee a single shared
* instance across all module copies — critical when TS plugins are loaded via
* npm link / peerDependency symlinks.
*
* Available hooks:
* onStartup — fired once after all commands & plugins are discovered
* onBeforeExecute — fired before every command execution
* onAfterExecute — fired after every command execution (receives result)
*/
import { log } from './logger.js';
export type HookName = 'onStartup' | 'onBeforeExecute' | 'onAfterExecute';
export interface HookContext {
/** Command full name in "site/name" format, or "__startup__" for onStartup */
command: string;
/** Coerced and validated arguments */
args: Record<string, unknown>;
/** Epoch ms when execution started (set by executeCommand) */
startedAt?: number;
/** Epoch ms when execution finished (set by executeCommand) */
finishedAt?: number;
/** Error thrown by the command, if execution failed */
error?: unknown;
/** Plugins can attach arbitrary data here for cross-hook communication */
[key: string]: unknown;
}
export type HookFn = (ctx: HookContext, result?: unknown) => void | Promise<void>;
// ── Singleton hook store (shared across module instances via globalThis) ──
declare global {
// eslint-disable-next-line no-var
var __opencli_hooks__: Map<HookName, HookFn[]> | undefined;
}
const _hooks: Map<HookName, HookFn[]> =
globalThis.__opencli_hooks__ ??= new Map();
// ── Registration API (used by plugins) ─────────────────────────────────────
function addHook(name: HookName, fn: HookFn): void {
const list = _hooks.get(name) ?? [];
if (list.includes(fn)) return;
list.push(fn);
_hooks.set(name, list);
}
/** Register a hook that fires once after all plugins are discovered. */
export function onStartup(fn: HookFn): void {
addHook('onStartup', fn);
}
/** Register a hook that fires before every command execution. */
export function onBeforeExecute(fn: HookFn): void {
addHook('onBeforeExecute', fn);
}
/** Register a hook that fires after every command execution with the result. */
export function onAfterExecute(fn: HookFn): void {
addHook('onAfterExecute', fn);
}
// ── Emit API (used internally by opencli core) ─────────────────────────────
/**
* Trigger all registered handlers for a hook.
* Each handler is wrapped in try/catch — a failing hook never blocks command execution.
*/
export async function emitHook(name: HookName, ctx: HookContext, result?: unknown): Promise<void> {
const handlers = _hooks.get(name);
if (!handlers || handlers.length === 0) return;
for (const fn of handlers) {
try {
await fn(ctx, result);
} catch (err) {
log.warn(`Hook ${name} handler failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
}
/**
* Remove all registered hooks. Intended for testing only.
*/
export function clearAllHooks(): void {
_hooks.clear();
}