@vibe-forge/core 0.7.5 → 0.8.0
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/package.json +4 -46
- package/src/env.ts +5 -25
- package/src/index.ts +0 -5
- package/src/types.ts +12 -72
- package/src/ws.ts +3 -12
- package/src/adapter/index.ts +0 -6
- package/src/adapter/loader.ts +0 -11
- package/src/adapter/type.ts +0 -117
- package/src/config/load.ts +0 -122
- package/src/config/types.ts +0 -289
- package/src/config.ts +0 -2
- package/src/controllers/benchmark/discover.ts +0 -89
- package/src/controllers/benchmark/index.ts +0 -24
- package/src/controllers/benchmark/result-store.ts +0 -46
- package/src/controllers/benchmark/runner.ts +0 -415
- package/src/controllers/benchmark/schema.ts +0 -60
- package/src/controllers/benchmark/types.ts +0 -80
- package/src/controllers/benchmark/utils.ts +0 -144
- package/src/controllers/benchmark/workspace.ts +0 -179
- package/src/controllers/config/index.ts +0 -214
- package/src/controllers/system/assets/completed.mp3 +0 -0
- package/src/controllers/system/assets/mcp.png +0 -0
- package/src/controllers/system/index.ts +0 -102
- package/src/controllers/task/generate-adapter-query-options.ts +0 -25
- package/src/controllers/task/index.ts +0 -2
- package/src/controllers/task/prepare.ts +0 -74
- package/src/controllers/task/run.ts +0 -231
- package/src/controllers/task/schema.ts +0 -131
- package/src/controllers/task/type.ts +0 -6
- package/src/hooks/bridge.ts +0 -368
- package/src/hooks/call.ts +0 -74
- package/src/hooks/index.ts +0 -41
- package/src/hooks/loader.ts +0 -79
- package/src/hooks/native.ts +0 -116
- package/src/hooks/runtime.ts +0 -139
- package/src/hooks/type.ts +0 -145
- package/src/utils/cache.ts +0 -58
- package/src/utils/create-logger.ts +0 -89
- package/src/utils/definition-loader.ts +0 -530
- package/src/utils/filter.ts +0 -26
- package/src/utils/string-transform.ts +0 -37
- package/src/utils/uuid.ts +0 -6
- package/src/utils/workspace-assets.ts +0 -919
package/src/hooks/bridge.ts
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AdapterCtx,
|
|
3
|
-
AdapterEvent,
|
|
4
|
-
AdapterOutputEvent,
|
|
5
|
-
AdapterQueryOptions,
|
|
6
|
-
AdapterSession
|
|
7
|
-
} from '#~/adapter/index.js'
|
|
8
|
-
import type { ChatMessage, ChatMessageContent } from '#~/types.js'
|
|
9
|
-
import type { HookInputs } from './type'
|
|
10
|
-
|
|
11
|
-
import { callHook } from './call'
|
|
12
|
-
|
|
13
|
-
const normalizeText = (value: unknown) => (
|
|
14
|
-
typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
const stringifyUnknown = (value: unknown) => {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.stringify(value)
|
|
20
|
-
} catch {
|
|
21
|
-
return String(value)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const describeContentPart = (item: ChatMessageContent): string | undefined => {
|
|
26
|
-
switch (item.type) {
|
|
27
|
-
case 'text':
|
|
28
|
-
return normalizeText(item.text)
|
|
29
|
-
case 'image':
|
|
30
|
-
return normalizeText(item.name) != null ? `[Image: ${item.name}]` : '[Image attachment]'
|
|
31
|
-
case 'tool_use':
|
|
32
|
-
return `Tool request: ${item.name}`
|
|
33
|
-
case 'tool_result':
|
|
34
|
-
return typeof item.content === 'string' ? normalizeText(item.content) : stringifyUnknown(item.content)
|
|
35
|
-
default:
|
|
36
|
-
return undefined
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const extractPromptText = (content: AdapterEvent['content']) => {
|
|
41
|
-
const parts = content
|
|
42
|
-
.map(describeContentPart)
|
|
43
|
-
.filter((value): value is string => value != null)
|
|
44
|
-
|
|
45
|
-
return parts.length > 0 ? parts.join('\n\n') : undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const extractAssistantText = (message: ChatMessage | undefined) => {
|
|
49
|
-
if (message == null) return undefined
|
|
50
|
-
if (typeof message.content === 'string') return normalizeText(message.content)
|
|
51
|
-
|
|
52
|
-
const parts = message.content
|
|
53
|
-
.filter((item): item is Extract<ChatMessageContent, { type: 'text' }> => item.type === 'text')
|
|
54
|
-
.map(item => normalizeText(item.text))
|
|
55
|
-
.filter((value): value is string => value != null)
|
|
56
|
-
|
|
57
|
-
return parts.length > 0 ? parts.join('') : undefined
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const isToolUse = (
|
|
61
|
-
item: ChatMessageContent
|
|
62
|
-
): item is Extract<ChatMessageContent, { type: 'tool_use' }> => item.type === 'tool_use'
|
|
63
|
-
|
|
64
|
-
const isToolResult = (
|
|
65
|
-
item: ChatMessageContent
|
|
66
|
-
): item is Extract<ChatMessageContent, { type: 'tool_result' }> => item.type === 'tool_result'
|
|
67
|
-
|
|
68
|
-
interface HookOutputLike {
|
|
69
|
-
continue?: boolean
|
|
70
|
-
stopReason?: string
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface AdapterHookBridge {
|
|
74
|
-
start: () => Promise<void>
|
|
75
|
-
prepareInitialPrompt: (prompt?: string) => Promise<string | undefined>
|
|
76
|
-
wrapSession: (session: AdapterSession) => AdapterSession
|
|
77
|
-
handleOutput: (event: AdapterOutputEvent) => void
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const createAdapterHookBridge = (params: {
|
|
81
|
-
ctx: Pick<AdapterCtx, 'cwd' | 'env' | 'logger'>
|
|
82
|
-
adapter: string
|
|
83
|
-
runtime: AdapterQueryOptions['runtime']
|
|
84
|
-
sessionId: string
|
|
85
|
-
type: AdapterQueryOptions['type']
|
|
86
|
-
model?: string
|
|
87
|
-
disabledEvents?: Array<keyof HookInputs>
|
|
88
|
-
}): AdapterHookBridge => {
|
|
89
|
-
const {
|
|
90
|
-
ctx,
|
|
91
|
-
adapter,
|
|
92
|
-
runtime,
|
|
93
|
-
sessionId,
|
|
94
|
-
type,
|
|
95
|
-
model,
|
|
96
|
-
disabledEvents: rawDisabledEvents = []
|
|
97
|
-
} = params
|
|
98
|
-
const disabledEvents = new Set(rawDisabledEvents)
|
|
99
|
-
|
|
100
|
-
const pendingToolCalls = new Map<string, { toolName: string; toolInput?: unknown }>()
|
|
101
|
-
let emitQueue = Promise.resolve()
|
|
102
|
-
let killRequested = false
|
|
103
|
-
let lastAssistantMessage: string | undefined
|
|
104
|
-
let sessionEnded = false
|
|
105
|
-
|
|
106
|
-
const callBridgeHook = async (
|
|
107
|
-
eventName: Parameters<typeof callHook>[0],
|
|
108
|
-
input: Record<string, unknown>,
|
|
109
|
-
options: {
|
|
110
|
-
canBlock: boolean
|
|
111
|
-
enforce?: boolean
|
|
112
|
-
blockedMessage: string
|
|
113
|
-
}
|
|
114
|
-
): Promise<HookOutputLike | undefined> => {
|
|
115
|
-
if (disabledEvents.has(eventName as keyof HookInputs)) {
|
|
116
|
-
return undefined
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const output = await callHook(
|
|
121
|
-
eventName as never,
|
|
122
|
-
{
|
|
123
|
-
cwd: ctx.cwd,
|
|
124
|
-
sessionId,
|
|
125
|
-
adapter,
|
|
126
|
-
runtime,
|
|
127
|
-
hookSource: 'bridge',
|
|
128
|
-
canBlock: options.canBlock,
|
|
129
|
-
...input
|
|
130
|
-
} as never,
|
|
131
|
-
ctx.env
|
|
132
|
-
) as HookOutputLike
|
|
133
|
-
|
|
134
|
-
if (options.enforce && output?.continue === false) {
|
|
135
|
-
throw new Error(output.stopReason ?? options.blockedMessage)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!options.canBlock && output?.continue === false) {
|
|
139
|
-
ctx.logger.warn(
|
|
140
|
-
`[HookBridge] Ignoring blocking output from observational ${String(eventName)} hook`,
|
|
141
|
-
output.stopReason
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return output
|
|
146
|
-
} catch (error) {
|
|
147
|
-
ctx.logger.error(`[HookBridge] ${String(eventName)} failed`, error)
|
|
148
|
-
if (options.enforce) throw error
|
|
149
|
-
return undefined
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const observeMessage = (message: ChatMessage) => {
|
|
154
|
-
const assistantText = extractAssistantText(message)
|
|
155
|
-
if (assistantText != null) lastAssistantMessage = assistantText
|
|
156
|
-
const preToolUseDisabled = disabledEvents.has('PreToolUse')
|
|
157
|
-
const postToolUseDisabled = disabledEvents.has('PostToolUse')
|
|
158
|
-
const needsPendingToolCalls = !postToolUseDisabled
|
|
159
|
-
|
|
160
|
-
if (Array.isArray(message.content)) {
|
|
161
|
-
for (const item of message.content) {
|
|
162
|
-
if (isToolUse(item)) {
|
|
163
|
-
if (needsPendingToolCalls) {
|
|
164
|
-
pendingToolCalls.set(item.id, {
|
|
165
|
-
toolName: item.name,
|
|
166
|
-
toolInput: item.input
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
if (!preToolUseDisabled) {
|
|
170
|
-
void callBridgeHook(
|
|
171
|
-
'PreToolUse',
|
|
172
|
-
{
|
|
173
|
-
toolCallId: item.id,
|
|
174
|
-
toolName: item.name,
|
|
175
|
-
toolInput: item.input
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
canBlock: false,
|
|
179
|
-
blockedMessage: 'PreToolUse hook attempted to block an observed tool call'
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (isToolResult(item)) {
|
|
186
|
-
const pending = pendingToolCalls.get(item.tool_use_id)
|
|
187
|
-
pendingToolCalls.delete(item.tool_use_id)
|
|
188
|
-
if (!postToolUseDisabled) {
|
|
189
|
-
void callBridgeHook(
|
|
190
|
-
'PostToolUse',
|
|
191
|
-
{
|
|
192
|
-
toolCallId: item.tool_use_id,
|
|
193
|
-
toolName: pending?.toolName ?? 'unknown',
|
|
194
|
-
toolInput: pending?.toolInput,
|
|
195
|
-
toolResponse: item.content,
|
|
196
|
-
isError: item.is_error ?? false
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
canBlock: false,
|
|
200
|
-
blockedMessage: 'PostToolUse hook attempted to block an observed tool result'
|
|
201
|
-
}
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (message.toolCall?.name != null) {
|
|
209
|
-
const toolCallId = message.toolCall.id ?? message.id
|
|
210
|
-
if (needsPendingToolCalls && !pendingToolCalls.has(toolCallId)) {
|
|
211
|
-
pendingToolCalls.set(toolCallId, {
|
|
212
|
-
toolName: message.toolCall.name,
|
|
213
|
-
toolInput: message.toolCall.args
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!preToolUseDisabled) {
|
|
218
|
-
void callBridgeHook(
|
|
219
|
-
'PreToolUse',
|
|
220
|
-
{
|
|
221
|
-
toolCallId,
|
|
222
|
-
toolName: message.toolCall.name,
|
|
223
|
-
toolInput: message.toolCall.args
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
canBlock: false,
|
|
227
|
-
blockedMessage: 'PreToolUse hook attempted to block a legacy observed tool call'
|
|
228
|
-
}
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!postToolUseDisabled && (message.toolCall.output != null || message.toolCall.status === 'error')) {
|
|
233
|
-
pendingToolCalls.delete(toolCallId)
|
|
234
|
-
void callBridgeHook(
|
|
235
|
-
'PostToolUse',
|
|
236
|
-
{
|
|
237
|
-
toolCallId,
|
|
238
|
-
toolName: message.toolCall.name,
|
|
239
|
-
toolInput: message.toolCall.args,
|
|
240
|
-
toolResponse: message.toolCall.output,
|
|
241
|
-
isError: message.toolCall.status === 'error'
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
canBlock: false,
|
|
245
|
-
blockedMessage: 'PostToolUse hook attempted to block a legacy observed tool result'
|
|
246
|
-
}
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
start: async () => {
|
|
254
|
-
if (disabledEvents.has('SessionStart')) return
|
|
255
|
-
await callBridgeHook(
|
|
256
|
-
'SessionStart',
|
|
257
|
-
{
|
|
258
|
-
source: type === 'resume' ? 'resume' : 'startup',
|
|
259
|
-
model
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
canBlock: true,
|
|
263
|
-
enforce: true,
|
|
264
|
-
blockedMessage: 'SessionStart hook blocked session startup'
|
|
265
|
-
}
|
|
266
|
-
)
|
|
267
|
-
},
|
|
268
|
-
prepareInitialPrompt: async (prompt) => {
|
|
269
|
-
const normalizedPrompt = normalizeText(prompt)
|
|
270
|
-
if (normalizedPrompt == null) return normalizedPrompt
|
|
271
|
-
if (disabledEvents.has('UserPromptSubmit')) return normalizedPrompt
|
|
272
|
-
|
|
273
|
-
await callBridgeHook(
|
|
274
|
-
'UserPromptSubmit',
|
|
275
|
-
{ prompt: normalizedPrompt },
|
|
276
|
-
{
|
|
277
|
-
canBlock: true,
|
|
278
|
-
enforce: true,
|
|
279
|
-
blockedMessage: 'UserPromptSubmit hook blocked the initial prompt'
|
|
280
|
-
}
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
return normalizedPrompt
|
|
284
|
-
},
|
|
285
|
-
wrapSession: (session) => ({
|
|
286
|
-
kill: () => {
|
|
287
|
-
killRequested = true
|
|
288
|
-
session.kill()
|
|
289
|
-
},
|
|
290
|
-
emit: (event) => {
|
|
291
|
-
emitQueue = emitQueue
|
|
292
|
-
.catch((error) => {
|
|
293
|
-
ctx.logger.error('[HookBridge] emit queue failed', error)
|
|
294
|
-
})
|
|
295
|
-
.then(async () => {
|
|
296
|
-
if (event.type === 'message') {
|
|
297
|
-
const prompt = extractPromptText(event.content)
|
|
298
|
-
if (prompt != null && !disabledEvents.has('UserPromptSubmit')) {
|
|
299
|
-
const output = await callBridgeHook(
|
|
300
|
-
'UserPromptSubmit',
|
|
301
|
-
{ prompt },
|
|
302
|
-
{
|
|
303
|
-
canBlock: true,
|
|
304
|
-
blockedMessage: 'UserPromptSubmit hook blocked the outgoing prompt'
|
|
305
|
-
}
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
if (output?.continue === false) {
|
|
309
|
-
ctx.logger.warn(
|
|
310
|
-
'[HookBridge] Dropping outgoing message blocked by UserPromptSubmit hook',
|
|
311
|
-
output.stopReason
|
|
312
|
-
)
|
|
313
|
-
return
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
session.emit(event)
|
|
319
|
-
})
|
|
320
|
-
},
|
|
321
|
-
get pid() {
|
|
322
|
-
return session.pid
|
|
323
|
-
}
|
|
324
|
-
}),
|
|
325
|
-
handleOutput: (event) => {
|
|
326
|
-
if (event.type === 'message' && event.data.role === 'assistant') {
|
|
327
|
-
observeMessage(event.data)
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (event.type === 'stop') {
|
|
332
|
-
if (disabledEvents.has('Stop')) return
|
|
333
|
-
void callBridgeHook(
|
|
334
|
-
'Stop',
|
|
335
|
-
{
|
|
336
|
-
lastAssistantMessage: extractAssistantText(event.data) ?? lastAssistantMessage
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
canBlock: false,
|
|
340
|
-
blockedMessage: 'Stop hook attempted to control an observed stop event'
|
|
341
|
-
}
|
|
342
|
-
)
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (event.type === 'exit' && !sessionEnded) {
|
|
347
|
-
sessionEnded = true
|
|
348
|
-
void callBridgeHook(
|
|
349
|
-
'SessionEnd',
|
|
350
|
-
{
|
|
351
|
-
reason: killRequested
|
|
352
|
-
? 'terminated'
|
|
353
|
-
: (event.data.exitCode ?? 0) === 0
|
|
354
|
-
? 'completed'
|
|
355
|
-
: 'failed',
|
|
356
|
-
exitCode: event.data.exitCode,
|
|
357
|
-
stderr: event.data.stderr,
|
|
358
|
-
lastAssistantMessage
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
canBlock: false,
|
|
362
|
-
blockedMessage: 'SessionEnd hook attempted to control an observed session end'
|
|
363
|
-
}
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
package/src/hooks/call.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer'
|
|
2
|
-
import { spawn } from 'node:child_process'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import process from 'node:process'
|
|
5
|
-
|
|
6
|
-
import type { HookInputs, HookOutputs } from './type'
|
|
7
|
-
|
|
8
|
-
export type HookEventName = keyof HookInputs
|
|
9
|
-
|
|
10
|
-
type HookInputPayload<K extends HookEventName> = Omit<HookInputs[K], 'hookEventName'>
|
|
11
|
-
|
|
12
|
-
const pickHookEnv = (env: Record<string, unknown>): Record<string, string> => {
|
|
13
|
-
const result: Record<string, string> = {}
|
|
14
|
-
for (const [key, value] of Object.entries(env)) {
|
|
15
|
-
if (typeof value === 'string') {
|
|
16
|
-
result[key] = value
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return result
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const resolveHookCliJs = () => {
|
|
23
|
-
try {
|
|
24
|
-
const pkgJsonPath = require.resolve('@vibe-forge/cli/package.json')
|
|
25
|
-
return path.resolve(path.dirname(pkgJsonPath), 'call-hook.js')
|
|
26
|
-
} catch (error) {
|
|
27
|
-
throw new Error('Failed to resolve @vibe-forge/cli hook entry', { cause: error })
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const callHook = async <K extends HookEventName>(
|
|
32
|
-
hookEventName: K,
|
|
33
|
-
input: HookInputPayload<K>,
|
|
34
|
-
env: Record<string, unknown> = process.env
|
|
35
|
-
): Promise<HookOutputs[K]> => {
|
|
36
|
-
const childEnv = pickHookEnv(env)
|
|
37
|
-
const child = spawn(process.execPath, [resolveHookCliJs()], {
|
|
38
|
-
cwd: typeof input.cwd === 'string' ? input.cwd : process.cwd(),
|
|
39
|
-
env: childEnv,
|
|
40
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const stdoutChunks: Buffer[] = []
|
|
44
|
-
const stderrChunks: Buffer[] = []
|
|
45
|
-
|
|
46
|
-
child.stdout.on('data', chunk => stdoutChunks.push(chunk))
|
|
47
|
-
child.stderr.on('data', chunk => stderrChunks.push(chunk))
|
|
48
|
-
|
|
49
|
-
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
50
|
-
child.once('error', reject)
|
|
51
|
-
child.once('close', code => resolve(code ?? 0))
|
|
52
|
-
child.stdin.end(JSON.stringify({
|
|
53
|
-
...input,
|
|
54
|
-
hookEventName
|
|
55
|
-
}))
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const stdout = Buffer.concat(stdoutChunks).toString('utf-8').trim()
|
|
59
|
-
const stderr = Buffer.concat(stderrChunks).toString('utf-8').trim()
|
|
60
|
-
|
|
61
|
-
if (exitCode !== 0) {
|
|
62
|
-
throw new Error(`Failed to call hook: process exited with code ${exitCode}${stderr ? ` - ${stderr}` : ''}`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (stdout === '') {
|
|
66
|
-
return { continue: true } as HookOutputs[K]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
return JSON.parse(stdout) as HookOutputs[K]
|
|
71
|
-
} catch (error) {
|
|
72
|
-
throw new Error(`Failed to parse hook output: ${stdout}${stderr ? `\nstderr: ${stderr}` : ''}`, { cause: error })
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/hooks/index.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { Writable } from 'node:stream'
|
|
2
|
-
|
|
3
|
-
import type { HookInputs, HookOutputs } from './type'
|
|
4
|
-
|
|
5
|
-
export interface HookContext {
|
|
6
|
-
logger: {
|
|
7
|
-
stream: Writable
|
|
8
|
-
info: (...args: unknown[]) => void
|
|
9
|
-
warn: (...args: unknown[]) => void
|
|
10
|
-
debug: (...args: unknown[]) => void
|
|
11
|
-
error: (...args: unknown[]) => void
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type Plugin =
|
|
16
|
-
& {
|
|
17
|
-
name?: string
|
|
18
|
-
}
|
|
19
|
-
& {
|
|
20
|
-
[P in keyof HookInputs]: (
|
|
21
|
-
ctx: HookContext,
|
|
22
|
-
input: HookInputs[P],
|
|
23
|
-
next: () => Promise<HookOutputs[P]>
|
|
24
|
-
) => Promise<HookOutputs[P]>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const definePlugin = (plugin: Partial<Plugin>) => plugin
|
|
28
|
-
|
|
29
|
-
export interface PluginMap {}
|
|
30
|
-
|
|
31
|
-
export type PluginConfig =
|
|
32
|
-
| (Partial<Plugin> | (() => Partial<Plugin>))[]
|
|
33
|
-
| Record<string, Record<string, unknown>>
|
|
34
|
-
| Partial<PluginMap>
|
|
35
|
-
|
|
36
|
-
export * from './call'
|
|
37
|
-
export * from './bridge'
|
|
38
|
-
export * from './loader'
|
|
39
|
-
export * from './native'
|
|
40
|
-
export * from './runtime'
|
|
41
|
-
export * from './type'
|
package/src/hooks/loader.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import type { Plugin, PluginConfig } from './index'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 解析单个插件配置
|
|
5
|
-
*/
|
|
6
|
-
const loadPlugin = async (
|
|
7
|
-
name: string,
|
|
8
|
-
config: Record<string, unknown>
|
|
9
|
-
): Promise<Partial<Plugin> | null> => {
|
|
10
|
-
try {
|
|
11
|
-
// eslint-disable-next-line ts/no-require-imports
|
|
12
|
-
const module = require(`${name}/hooks`)
|
|
13
|
-
|
|
14
|
-
// 兼容 ESM default export 和 CJS module.exports
|
|
15
|
-
const factory: (
|
|
16
|
-
// 直接导出插件对象
|
|
17
|
-
| Partial<Plugin>
|
|
18
|
-
// 导出工厂函数,接受配置并返回插件对象
|
|
19
|
-
| ((config: Record<string, unknown>) => Partial<Plugin>)
|
|
20
|
-
) = module.default ?? module
|
|
21
|
-
|
|
22
|
-
if (typeof factory === 'function') {
|
|
23
|
-
// TODO: 这里可以注入更多上下文,如全局配置、版本信息等
|
|
24
|
-
return factory(config)
|
|
25
|
-
} else if (typeof factory === 'object' && factory !== null) {
|
|
26
|
-
return factory
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
console.warn(`Plugin ${name} does not export a valid plugin factory or object.`)
|
|
30
|
-
return null
|
|
31
|
-
} catch (e) {
|
|
32
|
-
console.error(`Failed to load plugin ${name}:`, e)
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const resolvePlugins = async (
|
|
38
|
-
config: PluginConfig,
|
|
39
|
-
enabledPlugins: Record<string, boolean> = {}
|
|
40
|
-
): Promise<Partial<Plugin>[]> => {
|
|
41
|
-
// 1. 处理数组形式配置 (直接实例化或函数调用)
|
|
42
|
-
if (Array.isArray(config)) {
|
|
43
|
-
return config.map((p) => (typeof p === 'function' ? p() : p))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 2. 处理对象形式配置 (动态加载)
|
|
47
|
-
const entries = Object.entries(config)
|
|
48
|
-
.filter(([pkgName]) => enabledPlugins[pkgName] !== false)
|
|
49
|
-
if (entries.length === 0) return []
|
|
50
|
-
|
|
51
|
-
// 并行加载所有插件
|
|
52
|
-
const modules = await Promise.allSettled(
|
|
53
|
-
entries.map(([pkgName, pkgConfig]) => {
|
|
54
|
-
// 如果不是以 @ 或 @vibe-forge/plugin- 开头,则默认加上 @vibe-forge/plugin- 前缀
|
|
55
|
-
const resolvedName = pkgName.startsWith('@') ? pkgName : `@vibe-forge/plugin-${pkgName}`
|
|
56
|
-
// dprint-ignore
|
|
57
|
-
return (
|
|
58
|
-
loadPlugin(resolvedName, pkgConfig as Record<string, unknown>) ??
|
|
59
|
-
loadPlugin(pkgName, pkgConfig as Record<string, unknown>)
|
|
60
|
-
)
|
|
61
|
-
})
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
// 收集成功加载的插件
|
|
65
|
-
const plugins: Partial<Plugin>[] = []
|
|
66
|
-
|
|
67
|
-
modules.forEach((result, index) => {
|
|
68
|
-
const pkgName = entries[index][0]
|
|
69
|
-
if (result.status === 'fulfilled') {
|
|
70
|
-
if (result.value) {
|
|
71
|
-
plugins.push(result.value)
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
console.error(`Error loading plugin ${pkgName}:`, result.reason)
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
return plugins
|
|
79
|
-
}
|
package/src/hooks/native.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { dirname, resolve } from 'node:path'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
|
|
5
|
-
import type { AdapterCtx } from '../adapter'
|
|
6
|
-
|
|
7
|
-
export interface NativeHookHandlerConfig {
|
|
8
|
-
type: 'command'
|
|
9
|
-
command: string
|
|
10
|
-
timeout?: number
|
|
11
|
-
statusMessage?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface NativeHookMatcherGroup {
|
|
15
|
-
matcher?: string
|
|
16
|
-
hooks?: NativeHookHandlerConfig[]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface NativeHooksConfigFile {
|
|
20
|
-
hooks?: Partial<Record<string, NativeHookMatcherGroup[]>> & Record<string, unknown>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const isPlainObject = (value: unknown): value is Record<string, unknown> => (
|
|
24
|
-
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
export const hasManagedHookPlugins = (
|
|
28
|
-
ctx: Pick<AdapterCtx, 'assets'>
|
|
29
|
-
) => (ctx.assets?.hookPlugins.length ?? 0) > 0
|
|
30
|
-
|
|
31
|
-
export const resolveMockHome = (
|
|
32
|
-
cwd: string,
|
|
33
|
-
env: Pick<AdapterCtx, 'env'>['env']
|
|
34
|
-
) => {
|
|
35
|
-
const explicitHome = env.HOME?.trim() || process.env.HOME?.trim()
|
|
36
|
-
return explicitHome ? resolve(explicitHome) : resolve(cwd, '.ai', '.mock')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const resolveHookCliPackageDir = () => {
|
|
40
|
-
try {
|
|
41
|
-
const pkgJsonPath = require.resolve('@vibe-forge/cli/package.json')
|
|
42
|
-
return dirname(pkgJsonPath)
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw new Error('Failed to resolve @vibe-forge/cli for native hooks', { cause: error })
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const resolveHookCliScriptPath = (fileName: string) => (
|
|
49
|
-
resolve(resolveHookCliPackageDir(), fileName)
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
export const shellQuote = (value: string) => JSON.stringify(value)
|
|
53
|
-
|
|
54
|
-
export const buildNodeScriptCommand = (params: {
|
|
55
|
-
nodePath: string
|
|
56
|
-
scriptPath: string
|
|
57
|
-
}) => `${shellQuote(params.nodePath)} ${shellQuote(params.scriptPath)}`
|
|
58
|
-
|
|
59
|
-
export const prepareManagedHookRuntime = (
|
|
60
|
-
ctx: Pick<AdapterCtx, 'cwd' | 'env'>
|
|
61
|
-
) => {
|
|
62
|
-
const mockHome = resolveMockHome(ctx.cwd, ctx.env)
|
|
63
|
-
const cliPackageDir = resolveHookCliPackageDir()
|
|
64
|
-
const nodePath = process.execPath
|
|
65
|
-
|
|
66
|
-
ctx.env.__VF_PROJECT_CLI_PACKAGE_DIR__ = cliPackageDir
|
|
67
|
-
ctx.env.__VF_PROJECT_WORKSPACE_FOLDER__ = ctx.env.__VF_PROJECT_WORKSPACE_FOLDER__ ?? ctx.cwd
|
|
68
|
-
ctx.env.__VF_PROJECT_NODE_PATH__ = nodePath
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
mockHome,
|
|
72
|
-
cliPackageDir,
|
|
73
|
-
nodePath
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const readJsonFileOrDefault = async <T>(filePath: string, fallback: T): Promise<T> => {
|
|
78
|
-
try {
|
|
79
|
-
return JSON.parse(await readFile(filePath, 'utf8')) as T
|
|
80
|
-
} catch (error) {
|
|
81
|
-
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return fallback
|
|
82
|
-
throw error
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export const writeJsonFile = async (filePath: string, value: unknown) => {
|
|
87
|
-
await mkdir(dirname(filePath), { recursive: true })
|
|
88
|
-
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const mergeManagedHookGroups = (params: {
|
|
92
|
-
existing: unknown
|
|
93
|
-
eventNames: readonly string[]
|
|
94
|
-
enabled: boolean
|
|
95
|
-
isManagedGroup: (group: NativeHookMatcherGroup) => boolean
|
|
96
|
-
createGroup: (eventName: string) => NativeHookMatcherGroup
|
|
97
|
-
shouldManageEvent?: (eventName: string) => boolean
|
|
98
|
-
}): NativeHooksConfigFile => {
|
|
99
|
-
const parsed = isPlainObject(params.existing) ? params.existing as NativeHooksConfigFile : {}
|
|
100
|
-
const hooks = isPlainObject(parsed.hooks) ? parsed.hooks : {}
|
|
101
|
-
const nextHooks: Record<string, unknown> = { ...hooks }
|
|
102
|
-
|
|
103
|
-
for (const eventName of params.eventNames) {
|
|
104
|
-
const groups = Array.isArray(hooks[eventName])
|
|
105
|
-
? (hooks[eventName] as NativeHookMatcherGroup[]).filter(group => !params.isManagedGroup(group))
|
|
106
|
-
: []
|
|
107
|
-
nextHooks[eventName] = params.enabled && (params.shouldManageEvent?.(eventName) ?? true)
|
|
108
|
-
? [...groups, params.createGroup(eventName)]
|
|
109
|
-
: groups
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
...parsed,
|
|
114
|
-
hooks: nextHooks
|
|
115
|
-
}
|
|
116
|
-
}
|