ring/platforms/opencode/plugin/hooks/registry.ts
Gandalf f85aa61440
feat: add OpenCode platform integration (consolidate ring-for-opencode)
Move OpenCode runtime plugin and installer into Ring monorepo under platforms/opencode/.
The installer reads skills, agents, and commands directly from the Ring monorepo's
canonical directories (default/, dev-team/, pm-team/, etc.) — zero asset duplication.

What's included:
- installer.sh: reads from Ring dirs, applies frontmatter/tool transforms, installs to ~/.config/opencode/
- plugin/: TypeScript runtime (RingUnifiedPlugin) with hooks, lifecycle, loaders
- src/: CLI (doctor, config-manager)
- prompts/: session-start and context-injection templates
- standards/: coding standards (from dev-team/docs/)
- ring.jsonc: default config with full 86-skill/35-agent/33-command inventory

What's NOT included (intentionally):
- assets/ directory: eliminated, content comes from Ring monorepo
- scripts/codereview/: eliminated, replaced by mithril
- using-ring-opencode skill: uses canonical using-ring instead

Transforms applied by installer:
- Agent: type→mode, strip version/changelog/output_schema/input_schema
- Skill: keep name+description frontmatter, body unchanged
- Command: strip argument-hint (unsupported by OpenCode)
- All: normalize tool names (Bash→bash, Read→read, etc.)
- All: strip Model Requirement sections from agents

Replaces: LerianStudio/ring-for-opencode repository
Generated-by: Gandalf
AI-Model: claude-opus-4
2026-03-07 22:46:47 -03:00

251 lines
5.5 KiB
TypeScript

/**
* Ring Hook Registry
*
* Manages hook registration, instantiation, and lifecycle execution.
* Implements middleware pattern for extensible hook chains.
*/
import type {
Hook,
HookContext,
HookLifecycle,
HookName,
HookOutput,
HookRegistryEntry,
HookResult,
} from "./types.js"
/**
* Configuration interface for hook system.
*/
export interface HookConfig {
disabledHooks?: HookName[]
}
/**
* Registry for managing hooks and their lifecycle execution.
*/
export class HookRegistry {
/** Registered hook factories */
private factories: Map<HookName, HookRegistryEntry> = new Map()
/** Instantiated hooks */
private hooks: Map<HookName, Hook> = new Map()
/** Disabled hooks set */
private disabledHooks: Set<HookName> = new Set()
/**
* Register a hook factory with metadata.
*/
registerFactory(entry: HookRegistryEntry): void {
this.factories.set(entry.name, entry)
}
/**
* Get all registered factories.
*/
getFactories(): HookRegistryEntry[] {
return Array.from(this.factories.values())
}
/**
* Instantiate a hook from its factory.
*/
instantiate<TConfig extends Record<string, unknown> = Record<string, unknown>>(
name: HookName,
config?: TConfig,
): Hook | null {
const entry = this.factories.get(name)
if (!entry) {
return null
}
const hook = entry.factory(config as Record<string, unknown> | undefined)
// Respect disabled state
if (this.disabledHooks.has(name)) {
hook.enabled = false
}
this.hooks.set(name, hook)
return hook
}
/**
* Register an instantiated hook directly.
*/
register(hook: Hook): void {
// Respect disabled state
if (this.disabledHooks.has(hook.name)) {
hook.enabled = false
}
this.hooks.set(hook.name, hook)
}
/**
* Unregister a hook by name.
*/
unregister(name: HookName): void {
this.hooks.delete(name)
}
/**
* Get a hook by name.
*/
get(name: HookName): Hook | undefined {
return this.hooks.get(name)
}
/**
* Check if a hook is registered.
*/
has(name: HookName): boolean {
return this.hooks.has(name)
}
/**
* Disable a hook by name.
*/
disable(name: HookName): void {
this.disabledHooks.add(name)
const hook = this.hooks.get(name)
if (hook) {
hook.enabled = false
}
}
/**
* Enable a hook by name.
*/
enable(name: HookName): void {
this.disabledHooks.delete(name)
const hook = this.hooks.get(name)
if (hook) {
hook.enabled = true
}
}
/**
* Check if a hook is disabled.
*/
isDisabled(name: HookName): boolean {
return this.disabledHooks.has(name)
}
/**
* Set the disabled hooks from configuration.
*/
setDisabledHooks(names: HookName[]): void {
this.disabledHooks = new Set(names)
// Update existing hooks
for (const [hookName, hook] of this.hooks) {
hook.enabled = !this.disabledHooks.has(hookName)
}
}
/**
* Get all hooks that respond to a specific lifecycle event.
* Returns hooks sorted by priority (lower = earlier).
*/
getHooksForLifecycle(lifecycle: HookLifecycle): Hook[] {
const matchingHooks: Hook[] = []
for (const hook of this.hooks.values()) {
if (hook.enabled && hook.lifecycles.includes(lifecycle)) {
matchingHooks.push(hook)
}
}
// Sort by priority (lower = earlier, default 100)
// Use name as secondary key for deterministic ordering
return matchingHooks.sort((a, b) => {
const priorityDiff = (a.priority ?? 100) - (b.priority ?? 100)
if (priorityDiff !== 0) return priorityDiff
return a.name.localeCompare(b.name)
})
}
/**
* Execute all hooks for a lifecycle event in priority order.
* Implements chain-of-responsibility pattern.
*/
async executeLifecycle(
lifecycle: HookLifecycle,
ctx: HookContext,
output: HookOutput,
): Promise<HookResult[]> {
const hooks = this.getHooksForLifecycle(lifecycle)
const results: HookResult[] = []
let chainData: Record<string, unknown> = {}
for (const hook of hooks) {
try {
// Pass chain data from previous hooks
const contextWithChainData: HookContext = {
...ctx,
chainData,
}
const result = await hook.execute(contextWithChainData, output)
results.push(result)
// Accumulate chain data
if (result.data) {
chainData = { ...chainData, ...result.data }
}
// Stop chain if requested
if (result.stopChain) {
break
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
results.push({
success: false,
error: `Hook ${hook.name} failed: ${errorMessage}`,
})
}
}
return results
}
/**
* Get names of all registered hooks.
*/
getRegisteredNames(): HookName[] {
return Array.from(this.hooks.keys())
}
/**
* Get count of registered hooks.
*/
count(): number {
return this.hooks.size
}
/**
* Clear all registered hooks.
*/
clear(): void {
this.hooks.clear()
this.disabledHooks.clear()
}
}
/**
* Singleton hook registry instance.
*/
export const hookRegistry = new HookRegistry()
/**
* Check if a hook is disabled in the given configuration.
*/
export function isHookDisabled(config: HookConfig | undefined, hookName: HookName): boolean {
if (!config?.disabledHooks) {
return false
}
return config.disabledHooks.includes(hookName)
}