@nicerice/openhooks 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +4 -4
- package/package.json +5 -4
- package/src/application/hooks/create-session-handler.ts +11 -0
- package/src/application/hooks/create-tool-handler.ts +21 -0
- package/src/application/hooks/matches-tool.ts +7 -0
- package/src/application/hooks/partition-entries.ts +20 -0
- package/src/application/hooks/register-hooks.ts +19 -0
- package/src/application/hooks/resolve-label.ts +3 -0
- package/src/application/hooks/run-actions.ts +10 -0
- package/src/application/types/event-input.ts +3 -0
- package/src/application/types/tool-input.ts +6 -0
- package/src/application/types/tool-output.ts +5 -0
- package/src/domain/types/bash-action.ts +4 -0
- package/src/domain/types/event-types.ts +9 -0
- package/src/domain/types/exec-result.ts +5 -0
- package/src/domain/types/hook-entry.ts +9 -0
- package/src/domain/types/hooks-config.ts +6 -0
- package/src/domain/types/session-hook-entry.ts +8 -0
- package/src/infrastructure/config/append-action.ts +14 -0
- package/src/infrastructure/config/apply-property.ts +16 -0
- package/src/infrastructure/config/finalize-group.ts +3 -0
- package/src/infrastructure/config/load-config.ts +12 -0
- package/src/infrastructure/config/load-global.ts +13 -0
- package/src/infrastructure/config/load-project.ts +13 -0
- package/src/infrastructure/config/merge-by-id.ts +7 -0
- package/src/infrastructure/config/parse-bash-line.ts +7 -0
- package/src/infrastructure/config/parse-list-line.ts +7 -0
- package/src/infrastructure/config/parse-prop-line.ts +7 -0
- package/src/infrastructure/config/parse-tools-list.ts +3 -0
- package/src/infrastructure/config/parse-yaml.ts +18 -0
- package/src/infrastructure/config/paths.ts +6 -0
- package/src/infrastructure/config/process-line.ts +26 -0
- package/src/infrastructure/config/skip-line.ts +3 -0
- package/src/infrastructure/executor.ts +2 -2
- package/src/application/hooks.ts +0 -81
- package/src/domain/types.ts +0 -37
- package/src/infrastructure/config.ts +0 -73
package/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { loadConfig } from "./src/infrastructure/config.ts"
|
|
2
|
-
import { registerHooks } from "./src/application/hooks.ts"
|
|
1
|
+
import { loadConfig } from "./src/infrastructure/config/load-config.ts"
|
|
2
|
+
import { registerHooks } from "./src/application/hooks/register-hooks.ts"
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export function OpenhooksPlugin(ctx: { worktree?: string; directory?: string; project?: { root?: string } }) {
|
|
5
5
|
const root = ctx.worktree || ctx.directory || ctx.project?.root || process.cwd()
|
|
6
6
|
const config = loadConfig(root)
|
|
7
|
+
|
|
7
8
|
return registerHooks(config)
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export default OpenhooksPlugin
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nicerice/openhooks",
|
|
3
3
|
"description": "[opencode plugin] Hook any command into opencode's tool events.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"test": "bun test"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@types/bun": "latest"
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"@types/node": "^26.1.1",
|
|
24
|
+
"typescript": "^7.0.2"
|
|
25
|
+
}
|
|
25
26
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionHookEntry } from "../../domain/types/session-hook-entry.ts"
|
|
2
|
+
import type { EventInput } from "../types/event-input.ts"
|
|
3
|
+
import { resolveLabel } from "./resolve-label.ts"
|
|
4
|
+
import { runActions } from "./run-actions.ts"
|
|
5
|
+
|
|
6
|
+
export function createSessionHandler(entries: SessionHookEntry[]) {
|
|
7
|
+
return (input: EventInput) => entries.forEach(e => {
|
|
8
|
+
if (input.event.type !== e.event && input.event.type !== e.event.replace("session.", "")) return
|
|
9
|
+
try { runActions(e.actions) } catch {}
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EventType } from "../../domain/types/event-types.ts"
|
|
2
|
+
import type { HookEntry } from "../../domain/types/hook-entry.ts"
|
|
3
|
+
import type { ToolInput } from "../types/tool-input.ts"
|
|
4
|
+
import type { ToolOutput } from "../types/tool-output.ts"
|
|
5
|
+
import { matchesTool } from "./matches-tool.ts"
|
|
6
|
+
import { resolveLabel } from "./resolve-label.ts"
|
|
7
|
+
import { runActions } from "./run-actions.ts"
|
|
8
|
+
|
|
9
|
+
export function createToolHandler(event: string, entries: (HookEntry & { tools?: string[] })[]) {
|
|
10
|
+
return async (input: ToolInput, output?: ToolOutput) => {
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
if (!matchesTool(entry, input.tool)) continue
|
|
13
|
+
|
|
14
|
+
if (output) {
|
|
15
|
+
output.title = resolveLabel(entry, event as EventType)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
runActions(entry.actions, output)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HooksConfig } from "../../domain/types/hooks-config.ts"
|
|
2
|
+
import type { HookEntry } from "../../domain/types/hook-entry.ts"
|
|
3
|
+
import type { SessionHookEntry } from "../../domain/types/session-hook-entry.ts"
|
|
4
|
+
|
|
5
|
+
export function partitionEntries(config: HooksConfig) {
|
|
6
|
+
const toolHooks: Record<string, (HookEntry & { tools?: string[] })[]> = {}
|
|
7
|
+
const sessionHooks: SessionHookEntry[] = []
|
|
8
|
+
|
|
9
|
+
for (const entry of config.hooks) {
|
|
10
|
+
if (entry.event.startsWith("tool.")) {
|
|
11
|
+
if (!toolHooks[entry.event]) toolHooks[entry.event] = []
|
|
12
|
+
|
|
13
|
+
toolHooks[entry.event]!.push(entry as HookEntry & { tools?: string[] })
|
|
14
|
+
} else if (entry.event.startsWith("session.")) {
|
|
15
|
+
sessionHooks.push(entry as SessionHookEntry)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { toolHooks, sessionHooks }
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HooksConfig } from "../../domain/types/hooks-config.ts"
|
|
2
|
+
import { partitionEntries } from "./partition-entries.ts"
|
|
3
|
+
import { createToolHandler } from "./create-tool-handler.ts"
|
|
4
|
+
import { createSessionHandler } from "./create-session-handler.ts"
|
|
5
|
+
|
|
6
|
+
export function registerHooks(config: HooksConfig) {
|
|
7
|
+
const partitioned = partitionEntries(config)
|
|
8
|
+
const hooks: Record<string, Function> = {}
|
|
9
|
+
|
|
10
|
+
for (const [event, entries] of Object.entries(partitioned.toolHooks)) {
|
|
11
|
+
hooks[event] = createToolHandler(event, entries)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (partitioned.sessionHooks.length > 0) {
|
|
15
|
+
hooks.event = createSessionHandler(partitioned.sessionHooks)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return hooks
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { exec } from "../../infrastructure/executor.ts"
|
|
2
|
+
|
|
3
|
+
export function runActions(actions: { bash: string }[], label?: { output?: string }) {
|
|
4
|
+
for (const action of actions) {
|
|
5
|
+
const result = exec(action.bash)
|
|
6
|
+
const msg = result.stderr || result.stdout || ""
|
|
7
|
+
if (label) label.output = msg
|
|
8
|
+
if (result.exitCode !== 0) throw new Error(msg)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BashAction } from "../../domain/types/bash-action.ts"
|
|
2
|
+
import { parseBashLine } from "./parse-bash-line.ts"
|
|
3
|
+
|
|
4
|
+
export function appendAction(current: Record<string, unknown>, line: string) {
|
|
5
|
+
const parsed = parseBashLine(line)
|
|
6
|
+
|
|
7
|
+
if (!parsed) return false
|
|
8
|
+
|
|
9
|
+
if (!Array.isArray(current.actions)) current.actions = []
|
|
10
|
+
|
|
11
|
+
;(current.actions as BashAction[]).push(parsed)
|
|
12
|
+
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parsePropLine } from "./parse-prop-line.ts"
|
|
2
|
+
import { parseToolsList } from "./parse-tools-list.ts"
|
|
3
|
+
|
|
4
|
+
export function applyProperty(current: Record<string, unknown>, line: string) {
|
|
5
|
+
const prop = parsePropLine(line)
|
|
6
|
+
|
|
7
|
+
if (!prop) return
|
|
8
|
+
|
|
9
|
+
if (prop.key === "tools" && prop.value.startsWith("[")) {
|
|
10
|
+
current.tools = parseToolsList(prop.value)
|
|
11
|
+
} else if (prop.key === "actions") {
|
|
12
|
+
if (!Array.isArray(current.actions)) current.actions = []
|
|
13
|
+
} else {
|
|
14
|
+
current[prop.key] = prop.value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HooksConfig } from "../../domain/types/hooks-config.ts"
|
|
2
|
+
import { loadGlobal } from "./load-global.ts"
|
|
3
|
+
import { loadProject } from "./load-project.ts"
|
|
4
|
+
import { mergeHooks } from "./merge-by-id.ts"
|
|
5
|
+
|
|
6
|
+
export function loadConfig(worktree: string) {
|
|
7
|
+
const global = loadGlobal()
|
|
8
|
+
const project = loadProject(worktree)
|
|
9
|
+
const merged = mergeHooks(global, project)
|
|
10
|
+
|
|
11
|
+
return { hooks: merged }
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import type { HookEntry } from "../../domain/types/hook-entry.ts"
|
|
3
|
+
import type { SessionHookEntry } from "../../domain/types/session-hook-entry.ts"
|
|
4
|
+
import { getGlobalPath } from "./paths.ts"
|
|
5
|
+
import { parseYaml } from "./parse-yaml.ts"
|
|
6
|
+
|
|
7
|
+
export function loadGlobal() {
|
|
8
|
+
if (!existsSync(getGlobalPath())) return []
|
|
9
|
+
|
|
10
|
+
const parsed = parseYaml(readFileSync(getGlobalPath(), "utf-8"))
|
|
11
|
+
|
|
12
|
+
return parsed.hooks
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { parseYaml } from "./parse-yaml.ts"
|
|
4
|
+
|
|
5
|
+
export function loadProject(worktree: string) {
|
|
6
|
+
const projectPath = join(worktree, ".opencode/hooks.yaml")
|
|
7
|
+
|
|
8
|
+
if (!existsSync(projectPath)) return []
|
|
9
|
+
|
|
10
|
+
const parsed = parseYaml(readFileSync(projectPath, "utf-8"))
|
|
11
|
+
|
|
12
|
+
return parsed.hooks
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HookEntry } from "../../domain/types/hook-entry.ts"
|
|
2
|
+
import type { SessionHookEntry } from "../../domain/types/session-hook-entry.ts"
|
|
3
|
+
|
|
4
|
+
export function mergeHooks(global: (HookEntry | SessionHookEntry)[], project: (HookEntry | SessionHookEntry)[]) {
|
|
5
|
+
const ids = new Set(project.map(h => h.id).filter(Boolean))
|
|
6
|
+
return [...global.filter(h => !ids.has(h.id)), ...project]
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HooksConfig } from "../../domain/types/hooks-config.ts"
|
|
2
|
+
import type { HookEntry } from "../../domain/types/hook-entry.ts"
|
|
3
|
+
import type { SessionHookEntry } from "../../domain/types/session-hook-entry.ts"
|
|
4
|
+
import { processLine } from "./process-line.ts"
|
|
5
|
+
import { finalizeGroup } from "./finalize-group.ts"
|
|
6
|
+
|
|
7
|
+
export function parseYaml(text: string) {
|
|
8
|
+
const hooks: (HookEntry | SessionHookEntry)[] = []
|
|
9
|
+
let current: Record<string, unknown> | null = null
|
|
10
|
+
|
|
11
|
+
for (const raw of text.split("\n")) {
|
|
12
|
+
current = processLine(raw, current, hooks)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
finalizeGroup(current, hooks)
|
|
16
|
+
|
|
17
|
+
return { hooks }
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isSkipLine } from "./skip-line.ts"
|
|
2
|
+
import { parseListLine } from "./parse-list-line.ts"
|
|
3
|
+
import { appendAction } from "./append-action.ts"
|
|
4
|
+
import { applyProperty } from "./apply-property.ts"
|
|
5
|
+
|
|
6
|
+
export function processLine(raw: string, current: Record<string, unknown> | null, hooks: Record<string, unknown>[]) {
|
|
7
|
+
const line = raw.trimEnd()
|
|
8
|
+
|
|
9
|
+
if (isSkipLine(line)) return current
|
|
10
|
+
|
|
11
|
+
const parsed = parseListLine(line)
|
|
12
|
+
|
|
13
|
+
if (parsed) {
|
|
14
|
+
if (current) hooks.push(current)
|
|
15
|
+
|
|
16
|
+
return { [parsed.key]: parsed.value }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!current) return current
|
|
20
|
+
|
|
21
|
+
if (appendAction(current, line)) return current
|
|
22
|
+
|
|
23
|
+
applyProperty(current, line)
|
|
24
|
+
|
|
25
|
+
return current
|
|
26
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process"
|
|
2
|
-
import type { ExecResult } from "../domain/types.ts"
|
|
2
|
+
import type { ExecResult } from "../domain/types/exec-result.ts"
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export function exec(command: string, timeoutMs = 60_000) {
|
|
5
5
|
const result = spawnSync(command, {
|
|
6
6
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7
7
|
encoding: "utf-8",
|
package/src/application/hooks.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { HooksConfig, HookEntry, SessionHookEntry, ExecResult } from "../domain/types.ts"
|
|
2
|
-
import { exec } from "../infrastructure/executor.ts"
|
|
3
|
-
|
|
4
|
-
type ToolInput = {
|
|
5
|
-
tool: string
|
|
6
|
-
sessionID: string
|
|
7
|
-
callID: string
|
|
8
|
-
args?: Record<string, unknown>
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type ToolOutput = {
|
|
12
|
-
title?: string
|
|
13
|
-
output?: string
|
|
14
|
-
metadata?: Record<string, unknown>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type EventInput = {
|
|
18
|
-
event: { type: string; properties?: Record<string, unknown> }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const runActions = (actions: { bash: string }[], label: string): void => {
|
|
22
|
-
for (const action of actions) {
|
|
23
|
-
const result: ExecResult = exec(action.bash)
|
|
24
|
-
if (result.exitCode === 0) continue
|
|
25
|
-
throw new Error(`openhooks: ${label} failed (exit ${result.exitCode})\n${result.stderr || result.stdout}`)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const registerHooks = (config: HooksConfig) => {
|
|
30
|
-
const toolHooks: Record<string, (HookEntry & { tools?: string[] })[]> = {}
|
|
31
|
-
const sessionHooks: SessionHookEntry[] = []
|
|
32
|
-
|
|
33
|
-
for (const entry of config.hooks) {
|
|
34
|
-
if ("tools" in entry) {
|
|
35
|
-
if (!toolHooks[entry.event]) toolHooks[entry.event] = []
|
|
36
|
-
toolHooks[entry.event]!.push(entry as HookEntry & { tools?: string[] })
|
|
37
|
-
} else if (entry.event.startsWith("session.")) {
|
|
38
|
-
sessionHooks.push(entry as SessionHookEntry)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const hooks: Record<string, (input: ToolInput | EventInput, output?: ToolOutput) => Promise<void>> = {}
|
|
43
|
-
|
|
44
|
-
for (const [event, entries] of Object.entries(toolHooks)) {
|
|
45
|
-
if (event === "tool.execute.before" || event === "tool.execute.after") {
|
|
46
|
-
hooks[event] = async (input: ToolInput, output?: ToolOutput) => {
|
|
47
|
-
const toolInput = input as ToolInput
|
|
48
|
-
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
if (entry.tools && !entry.tools.includes(toolInput.tool)) continue
|
|
51
|
-
|
|
52
|
-
if (output) {
|
|
53
|
-
output.title = entry.id || event
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
runActions(entry.actions, entry.id || event)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (sessionHooks.length > 0) {
|
|
63
|
-
hooks.event = async (input: EventInput) => {
|
|
64
|
-
const type = input.event.type
|
|
65
|
-
|
|
66
|
-
for (const entry of sessionHooks) {
|
|
67
|
-
const sessionEvent = entry.event.replace("session.", "")
|
|
68
|
-
if (type !== sessionEvent && type !== entry.event) continue
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
runActions(entry.actions, entry.id || entry.event)
|
|
72
|
-
} catch (e) {
|
|
73
|
-
// ponytail: don't throw for session events, just log
|
|
74
|
-
console.error(`openhooks: session hook failed: ${e}`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return hooks
|
|
81
|
-
}
|
package/src/domain/types.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export type EventType =
|
|
2
|
-
| "tool.execute.before"
|
|
3
|
-
| "tool.execute.after"
|
|
4
|
-
|
|
5
|
-
export type SessionEventType =
|
|
6
|
-
| "session.created"
|
|
7
|
-
| "session.deleted"
|
|
8
|
-
| "session.idle"
|
|
9
|
-
| "session.error"
|
|
10
|
-
|
|
11
|
-
export type BashAction = {
|
|
12
|
-
bash: string
|
|
13
|
-
timeout?: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type HookEntry = {
|
|
17
|
-
id?: string
|
|
18
|
-
event: EventType
|
|
19
|
-
tools?: string[]
|
|
20
|
-
actions: BashAction[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type SessionHookEntry = {
|
|
24
|
-
id?: string
|
|
25
|
-
event: SessionEventType
|
|
26
|
-
actions: BashAction[]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type HooksConfig = {
|
|
30
|
-
hooks: (HookEntry | SessionHookEntry)[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ExecResult = {
|
|
34
|
-
exitCode: number
|
|
35
|
-
stdout: string
|
|
36
|
-
stderr: string
|
|
37
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
2
|
-
import { homedir } from "node:os"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import type { HooksConfig, HookEntry, SessionHookEntry } from "../domain/types.ts"
|
|
5
|
-
|
|
6
|
-
const parseYaml = (text: string): HooksConfig => {
|
|
7
|
-
const hooks: (HookEntry | SessionHookEntry)[] = []
|
|
8
|
-
let current: Record<string, unknown> | null = null
|
|
9
|
-
const lines = text.split("\n")
|
|
10
|
-
|
|
11
|
-
for (const raw of lines) {
|
|
12
|
-
const line = raw.trimEnd()
|
|
13
|
-
if (line.trim() === "" || line.trimStart().startsWith("#")) continue
|
|
14
|
-
|
|
15
|
-
const listStart = line.match(/^ - (\w+):\s*(.*)/)
|
|
16
|
-
if (listStart) {
|
|
17
|
-
if (current) hooks.push(current as HookEntry | SessionHookEntry)
|
|
18
|
-
current = { [listStart[1]!]: listStart[2]!.trim() } as Record<string, unknown>
|
|
19
|
-
continue
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!current) continue
|
|
23
|
-
|
|
24
|
-
const propMatch = line.match(/^\s+(\w+):\s*(.*)/)
|
|
25
|
-
const bashMatch = line.match(/^\s+- bash:\s*(.*)/)
|
|
26
|
-
|
|
27
|
-
if (!propMatch && !bashMatch) continue
|
|
28
|
-
|
|
29
|
-
if (bashMatch) {
|
|
30
|
-
if (!current.actions || !Array.isArray(current.actions)) current.actions = []
|
|
31
|
-
;(current.actions as { bash: string }[]).push({ bash: bashMatch[1]!.trim().replace(/^["']|["']$/g, "") })
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const key = propMatch![1]!
|
|
36
|
-
const val = propMatch![2]!.trim()
|
|
37
|
-
|
|
38
|
-
if (key === "tools" && val.startsWith("[")) {
|
|
39
|
-
current.tools = val.slice(1, -1).split(",").map((s) => s.trim().replace(/['"]/g, ""))
|
|
40
|
-
} else if (key === "actions") {
|
|
41
|
-
if (!current.actions) current.actions = []
|
|
42
|
-
} else {
|
|
43
|
-
current[key] = val
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (current) hooks.push(current as HookEntry | SessionHookEntry)
|
|
48
|
-
|
|
49
|
-
return { hooks }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const GLOBAL_PATH = join(homedir(), ".config", "opencode", "hooks.yaml")
|
|
53
|
-
const PROJECT_PATH = ".opencode/hooks.yaml"
|
|
54
|
-
|
|
55
|
-
export const loadConfig = (worktree: string): HooksConfig => {
|
|
56
|
-
const allHooks: (HookEntry | SessionHookEntry)[] = []
|
|
57
|
-
|
|
58
|
-
if (existsSync(GLOBAL_PATH)) {
|
|
59
|
-
const global = parseYaml(readFileSync(GLOBAL_PATH, "utf-8"))
|
|
60
|
-
allHooks.push(...global.hooks)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const projectPath = join(worktree, PROJECT_PATH)
|
|
64
|
-
if (existsSync(projectPath)) {
|
|
65
|
-
const project = parseYaml(readFileSync(projectPath, "utf-8"))
|
|
66
|
-
const ids = new Set(project.hooks.map((h) => h.id).filter(Boolean))
|
|
67
|
-
const filtered = allHooks.filter((h) => !ids.has(h.id))
|
|
68
|
-
allHooks.length = 0
|
|
69
|
-
allHooks.push(...filtered, ...project.hooks)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { hooks: allHooks }
|
|
73
|
-
}
|