* Run every command hook that matches `event`, in parallel, and fold their * outputs into a single result. `fields` carries the event-specific input * (tool_name, prompt, trigger, …); base fields are added automatically. * Never throws — a misbehaving hook surfaces as a non-blocking message.
(event: HookEvent, fields: Record<string, unknown> = {})
| 144 | * Never throws — a misbehaving hook surfaces as a non-blocking message. |
| 145 | */ |
| 146 | async run(event: HookEvent, fields: Record<string, unknown> = {}): Promise<AggregatedHookResult> { |
| 147 | const matchers = this.ctx.config?.[event] |
| 148 | if (!matchers || matchers.length === 0) return EMPTY_RESULT |
| 149 | |
| 150 | const query = getMatchQuery(event, fields) |
| 151 | const selected: HookCommand[] = [] |
| 152 | for (const m of matchers) { |
| 153 | if (!m || !Array.isArray(m.hooks)) continue |
| 154 | if (!matcherMatches(m.matcher, query)) continue |
| 155 | for (const h of m.hooks) { |
| 156 | if (h && h.type === "command" && typeof h.command === "string" && h.command.trim()) { |
| 157 | selected.push(h) |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | if (selected.length === 0) return EMPTY_RESULT |
| 162 | |
| 163 | const input = |
| 164 | JSON.stringify({ |
| 165 | session_id: this.ctx.sessionId, |
| 166 | transcript_path: this.ctx.transcriptPath, |
| 167 | cwd: this.ctx.cwd, |
| 168 | hook_event_name: event, |
| 169 | ...fields, |
| 170 | }) + "\n" |
| 171 | const env = buildHookEnv(this.ctx.cwd) |
| 172 | |
| 173 | const results = await Promise.all(selected.map((h) => this.runOne(event, h, input, env))) |
| 174 | return aggregate(results) |
| 175 | } |
| 176 | |
| 177 | private surface(message: string, isError: boolean): void { |
| 178 | this.ctx.onSystemMessage?.(message, isError) |
no test coverage detected