@armadra/agent 0.0.0-stage → 0.2.1
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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +426 -2
- package/dist/agent/agent.d.ts +56 -0
- package/dist/agent/agent.js +129 -0
- package/dist/agent/loop.d.ts +51 -0
- package/dist/agent/loop.js +158 -0
- package/dist/agent/queue.d.ts +25 -0
- package/dist/agent/queue.js +55 -0
- package/dist/agent/retry.d.ts +22 -0
- package/dist/agent/retry.js +92 -0
- package/dist/agent/schema.d.ts +27 -0
- package/dist/agent/schema.js +186 -0
- package/dist/agent/session-cache.d.ts +100 -0
- package/dist/agent/session-cache.js +511 -0
- package/dist/agent/session-compaction.d.ts +49 -0
- package/dist/agent/session-compaction.js +251 -0
- package/dist/agent/session-core.d.ts +103 -0
- package/dist/agent/session-core.js +7 -0
- package/dist/agent/session-run.d.ts +55 -0
- package/dist/agent/session-run.js +246 -0
- package/dist/agent/session-state.d.ts +39 -0
- package/dist/agent/session-state.js +118 -0
- package/dist/agent/session-subagent.d.ts +54 -0
- package/dist/agent/session-subagent.js +157 -0
- package/dist/agent/session-sync.d.ts +20 -0
- package/dist/agent/session-sync.js +104 -0
- package/dist/agent/session-tools.d.ts +24 -0
- package/dist/agent/session-tools.js +277 -0
- package/dist/agent/session.d.ts +117 -0
- package/dist/agent/session.js +484 -0
- package/dist/agent/system-prompt.d.ts +49 -0
- package/dist/agent/system-prompt.js +121 -0
- package/dist/agent/tool-runner.d.ts +64 -0
- package/dist/agent/tool-runner.js +309 -0
- package/dist/agent/transform.d.ts +26 -0
- package/dist/agent/transform.js +171 -0
- package/dist/agent/types.d.ts +378 -0
- package/dist/agent/types.js +21 -0
- package/dist/ai/apis/anthropic-messages.d.ts +26 -0
- package/dist/ai/apis/anthropic-messages.js +214 -0
- package/dist/ai/apis/anthropic-request.d.ts +42 -0
- package/dist/ai/apis/anthropic-request.js +282 -0
- package/dist/ai/apis/api.d.ts +28 -0
- package/dist/ai/apis/api.js +102 -0
- package/dist/ai/apis/cache-params.d.ts +48 -0
- package/dist/ai/apis/cache-params.js +163 -0
- package/dist/ai/apis/google-generative-ai.d.ts +32 -0
- package/dist/ai/apis/google-generative-ai.js +249 -0
- package/dist/ai/apis/google-request.d.ts +40 -0
- package/dist/ai/apis/google-request.js +255 -0
- package/dist/ai/apis/openai-compat.d.ts +35 -0
- package/dist/ai/apis/openai-compat.js +142 -0
- package/dist/ai/apis/openai-completions.d.ts +28 -0
- package/dist/ai/apis/openai-completions.js +246 -0
- package/dist/ai/apis/openai-request.d.ts +35 -0
- package/dist/ai/apis/openai-request.js +303 -0
- package/dist/ai/apis/openai-responses-request.d.ts +52 -0
- package/dist/ai/apis/openai-responses-request.js +266 -0
- package/dist/ai/apis/openai-responses.d.ts +28 -0
- package/dist/ai/apis/openai-responses.js +303 -0
- package/dist/ai/apis/shared.d.ts +56 -0
- package/dist/ai/apis/shared.js +179 -0
- package/dist/ai/cache/economics.d.ts +24 -0
- package/dist/ai/cache/economics.js +69 -0
- package/dist/ai/cache/fingerprint.d.ts +17 -0
- package/dist/ai/cache/fingerprint.js +39 -0
- package/dist/ai/cache/miss.d.ts +49 -0
- package/dist/ai/cache/miss.js +96 -0
- package/dist/ai/cache/reporting.d.ts +39 -0
- package/dist/ai/cache/reporting.js +102 -0
- package/dist/ai/cache/types.d.ts +94 -0
- package/dist/ai/cache/types.js +19 -0
- package/dist/ai/cache/warmer.d.ts +94 -0
- package/dist/ai/cache/warmer.js +224 -0
- package/dist/ai/context.d.ts +35 -0
- package/dist/ai/context.js +114 -0
- package/dist/ai/cost.d.ts +21 -0
- package/dist/ai/cost.js +60 -0
- package/dist/ai/event-stream.d.ts +33 -0
- package/dist/ai/event-stream.js +101 -0
- package/dist/ai/fake/fake-provider.d.ts +68 -0
- package/dist/ai/fake/fake-provider.js +278 -0
- package/dist/ai/fake/fake-script.d.ts +69 -0
- package/dist/ai/fake/fake-script.js +142 -0
- package/dist/ai/http.d.ts +54 -0
- package/dist/ai/http.js +212 -0
- package/dist/ai/json-partial.d.ts +17 -0
- package/dist/ai/json-partial.js +243 -0
- package/dist/ai/overflow.d.ts +22 -0
- package/dist/ai/overflow.js +61 -0
- package/dist/ai/providers/auth.d.ts +71 -0
- package/dist/ai/providers/auth.js +202 -0
- package/dist/ai/providers/builtin.d.ts +23 -0
- package/dist/ai/providers/builtin.js +148 -0
- package/dist/ai/providers/catalog-data.d.ts +5 -0
- package/dist/ai/providers/catalog-data.js +19 -0
- package/dist/ai/providers/catalog.d.ts +28 -0
- package/dist/ai/providers/catalog.js +166 -0
- package/dist/ai/providers/registry.d.ts +83 -0
- package/dist/ai/providers/registry.js +305 -0
- package/dist/ai/sse.d.ts +32 -0
- package/dist/ai/sse.js +140 -0
- package/dist/ai/thinking.d.ts +42 -0
- package/dist/ai/thinking.js +101 -0
- package/dist/ai/types.d.ts +428 -0
- package/dist/ai/types.js +19 -0
- package/dist/bundle/ama-sandbox.cjs +324 -0
- package/dist/bundle/ama.cjs +25770 -0
- package/dist/bundle.d.ts +9 -0
- package/dist/bundle.js +15 -0
- package/dist/cli/args.d.ts +82 -0
- package/dist/cli/args.js +381 -0
- package/dist/cli/bootstrap.d.ts +18 -0
- package/dist/cli/bootstrap.js +398 -0
- package/dist/cli/compose-providers.d.ts +37 -0
- package/dist/cli/compose-providers.js +104 -0
- package/dist/cli/compose-session.d.ts +69 -0
- package/dist/cli/compose-session.js +370 -0
- package/dist/cli/compose-store.d.ts +24 -0
- package/dist/cli/compose-store.js +125 -0
- package/dist/cli/compose.d.ts +106 -0
- package/dist/cli/compose.js +206 -0
- package/dist/cli/default-model.d.ts +16 -0
- package/dist/cli/default-model.js +24 -0
- package/dist/cli/deps.d.ts +205 -0
- package/dist/cli/deps.js +12 -0
- package/dist/cli/exit-codes.d.ts +29 -0
- package/dist/cli/exit-codes.js +40 -0
- package/dist/cli/main.d.ts +26 -0
- package/dist/cli/main.js +165 -0
- package/dist/cli/runtime.d.ts +90 -0
- package/dist/cli/runtime.js +8 -0
- package/dist/cli/startup-screen.d.ts +15 -0
- package/dist/cli/startup-screen.js +56 -0
- package/dist/cli/startup-steps.d.ts +35 -0
- package/dist/cli/startup-steps.js +197 -0
- package/dist/cli/subcommands/auth.d.ts +12 -0
- package/dist/cli/subcommands/auth.js +91 -0
- package/dist/cli/subcommands/config.d.ts +44 -0
- package/dist/cli/subcommands/config.js +201 -0
- package/dist/cli/subcommands/context.d.ts +21 -0
- package/dist/cli/subcommands/context.js +41 -0
- package/dist/cli/subcommands/doctor.d.ts +11 -0
- package/dist/cli/subcommands/doctor.js +246 -0
- package/dist/cli/subcommands/models-cache-probe.d.ts +34 -0
- package/dist/cli/subcommands/models-cache-probe.js +190 -0
- package/dist/cli/subcommands/models-discover.d.ts +40 -0
- package/dist/cli/subcommands/models-discover.js +204 -0
- package/dist/cli/subcommands/models.d.ts +40 -0
- package/dist/cli/subcommands/models.js +144 -0
- package/dist/cli/subcommands/sessions.d.ts +11 -0
- package/dist/cli/subcommands/sessions.js +100 -0
- package/dist/codemode/capability.d.ts +37 -0
- package/dist/codemode/capability.js +55 -0
- package/dist/codemode/declarations.d.ts +44 -0
- package/dist/codemode/declarations.js +111 -0
- package/dist/codemode/host-side.d.ts +59 -0
- package/dist/codemode/host-side.js +253 -0
- package/dist/codemode/modes.d.ts +23 -0
- package/dist/codemode/modes.js +40 -0
- package/dist/codemode/protocol.d.ts +91 -0
- package/dist/codemode/protocol.js +114 -0
- package/dist/codemode/sandbox-entry.d.ts +25 -0
- package/dist/codemode/sandbox-entry.js +323 -0
- package/dist/codemode/store.d.ts +26 -0
- package/dist/codemode/store.js +53 -0
- package/dist/codemode/tool.d.ts +105 -0
- package/dist/codemode/tool.js +283 -0
- package/dist/compaction/branch-summary.d.ts +30 -0
- package/dist/compaction/branch-summary.js +79 -0
- package/dist/compaction/breaker.d.ts +32 -0
- package/dist/compaction/breaker.js +65 -0
- package/dist/compaction/cut-point.d.ts +23 -0
- package/dist/compaction/cut-point.js +70 -0
- package/dist/compaction/estimate.d.ts +30 -0
- package/dist/compaction/estimate.js +114 -0
- package/dist/compaction/prune-tier.d.ts +31 -0
- package/dist/compaction/prune-tier.js +83 -0
- package/dist/compaction/serialize.d.ts +20 -0
- package/dist/compaction/serialize.js +115 -0
- package/dist/compaction/summarize-tier.d.ts +111 -0
- package/dist/compaction/summarize-tier.js +305 -0
- package/dist/config/auth-file.d.ts +39 -0
- package/dist/config/auth-file.js +92 -0
- package/dist/config/context-files.d.ts +30 -0
- package/dist/config/context-files.js +77 -0
- package/dist/config/load.d.ts +45 -0
- package/dist/config/load.js +97 -0
- package/dist/config/merge.d.ts +73 -0
- package/dist/config/merge.js +248 -0
- package/dist/config/paths.d.ts +38 -0
- package/dist/config/paths.js +108 -0
- package/dist/config/profile.d.ts +27 -0
- package/dist/config/profile.js +65 -0
- package/dist/config/schema.d.ts +34 -0
- package/dist/config/schema.js +413 -0
- package/dist/config/trust.d.ts +42 -0
- package/dist/config/trust.js +122 -0
- package/dist/config/types.d.ts +172 -0
- package/dist/config/types.js +27 -0
- package/dist/config/write.d.ts +10 -0
- package/dist/config/write.js +25 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +30 -0
- package/dist/hooks/config.d.ts +49 -0
- package/dist/hooks/config.js +102 -0
- package/dist/hooks/dispatcher.d.ts +41 -0
- package/dist/hooks/dispatcher.js +104 -0
- package/dist/hooks/matcher.d.ts +22 -0
- package/dist/hooks/matcher.js +108 -0
- package/dist/hooks/protocol.d.ts +33 -0
- package/dist/hooks/protocol.js +240 -0
- package/dist/hooks/runner.d.ts +30 -0
- package/dist/hooks/runner.js +143 -0
- package/dist/hooks/types.d.ts +147 -0
- package/dist/hooks/types.js +20 -0
- package/dist/host/api-impl.d.ts +75 -0
- package/dist/host/api-impl.js +173 -0
- package/dist/host/loader.d.ts +31 -0
- package/dist/host/loader.js +147 -0
- package/dist/host/types.d.ts +169 -0
- package/dist/host/types.js +14 -0
- package/dist/host.d.ts +5 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +28 -0
- package/dist/modes/commands-core.d.ts +44 -0
- package/dist/modes/commands-core.js +171 -0
- package/dist/modes/interactive/approval-dialog.d.ts +48 -0
- package/dist/modes/interactive/approval-dialog.js +202 -0
- package/dist/modes/interactive/commands.d.ts +41 -0
- package/dist/modes/interactive/commands.js +202 -0
- package/dist/modes/interactive/completion.d.ts +53 -0
- package/dist/modes/interactive/completion.js +157 -0
- package/dist/modes/interactive/interactive-mode.d.ts +46 -0
- package/dist/modes/interactive/interactive-mode.js +487 -0
- package/dist/modes/interactive/key-dispatch.d.ts +29 -0
- package/dist/modes/interactive/key-dispatch.js +107 -0
- package/dist/modes/interactive/line/line-editor.d.ts +50 -0
- package/dist/modes/interactive/line/line-editor.js +232 -0
- package/dist/modes/interactive/line/line-mode.d.ts +20 -0
- package/dist/modes/interactive/line/line-mode.js +216 -0
- package/dist/modes/interactive/line/line-render.d.ts +35 -0
- package/dist/modes/interactive/line/line-render.js +171 -0
- package/dist/modes/interactive/line/paste-state.d.ts +26 -0
- package/dist/modes/interactive/line/paste-state.js +72 -0
- package/dist/modes/interactive/message-view.d.ts +77 -0
- package/dist/modes/interactive/message-view.js +269 -0
- package/dist/modes/interactive/pickers.d.ts +42 -0
- package/dist/modes/interactive/pickers.js +115 -0
- package/dist/modes/interactive/startup-ui.d.ts +28 -0
- package/dist/modes/interactive/startup-ui.js +238 -0
- package/dist/modes/interactive/status-bar.d.ts +48 -0
- package/dist/modes/interactive/status-bar.js +139 -0
- package/dist/modes/interactive/tool-view.d.ts +95 -0
- package/dist/modes/interactive/tool-view.js +344 -0
- package/dist/modes/print/json-event.d.ts +15 -0
- package/dist/modes/print/json-event.js +39 -0
- package/dist/modes/print/print-mode.d.ts +15 -0
- package/dist/modes/print/print-mode.js +96 -0
- package/dist/modes/rpc/commands.d.ts +40 -0
- package/dist/modes/rpc/commands.js +258 -0
- package/dist/modes/rpc/jsonl.d.ts +16 -0
- package/dist/modes/rpc/jsonl.js +55 -0
- package/dist/modes/rpc/rpc-mode.d.ts +22 -0
- package/dist/modes/rpc/rpc-mode.js +153 -0
- package/dist/modes/session-report.d.ts +55 -0
- package/dist/modes/session-report.js +278 -0
- package/dist/modes/shared.d.ts +13 -0
- package/dist/modes/shared.js +52 -0
- package/dist/modes/startup-ui-text.d.ts +16 -0
- package/dist/modes/startup-ui-text.js +103 -0
- package/dist/permissions/broker.d.ts +47 -0
- package/dist/permissions/broker.js +107 -0
- package/dist/permissions/dangerous.d.ts +47 -0
- package/dist/permissions/dangerous.js +370 -0
- package/dist/permissions/pipeline.d.ts +51 -0
- package/dist/permissions/pipeline.js +181 -0
- package/dist/permissions/preview.d.ts +43 -0
- package/dist/permissions/preview.js +463 -0
- package/dist/permissions/rules.d.ts +58 -0
- package/dist/permissions/rules.js +249 -0
- package/dist/permissions/types.d.ts +100 -0
- package/dist/permissions/types.js +18 -0
- package/dist/rpc.d.ts +171 -0
- package/dist/rpc.js +13 -0
- package/dist/sdk.d.ts +98 -0
- package/dist/sdk.js +195 -0
- package/dist/session/manager.d.ts +70 -0
- package/dist/session/manager.js +278 -0
- package/dist/session/migrate.d.ts +19 -0
- package/dist/session/migrate.js +53 -0
- package/dist/session/projection.d.ts +50 -0
- package/dist/session/projection.js +180 -0
- package/dist/session/store.d.ts +47 -0
- package/dist/session/store.js +210 -0
- package/dist/session/tree.d.ts +21 -0
- package/dist/session/tree.js +88 -0
- package/dist/session/types.d.ts +199 -0
- package/dist/session/types.js +18 -0
- package/dist/skills/discover.d.ts +60 -0
- package/dist/skills/discover.js +160 -0
- package/dist/skills/expand.d.ts +35 -0
- package/dist/skills/expand.js +46 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +157 -0
- package/dist/skills/index-prompt.d.ts +13 -0
- package/dist/skills/index-prompt.js +42 -0
- package/dist/skills/templates.d.ts +52 -0
- package/dist/skills/templates.js +177 -0
- package/dist/tools/bash.d.ts +49 -0
- package/dist/tools/bash.js +222 -0
- package/dist/tools/edit-fuzzy.d.ts +36 -0
- package/dist/tools/edit-fuzzy.js +129 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +241 -0
- package/dist/tools/file-mutex.d.ts +8 -0
- package/dist/tools/file-mutex.js +29 -0
- package/dist/tools/glob.d.ts +44 -0
- package/dist/tools/glob.js +217 -0
- package/dist/tools/grep.d.ts +39 -0
- package/dist/tools/grep.js +179 -0
- package/dist/tools/ignore.d.ts +40 -0
- package/dist/tools/ignore.js +174 -0
- package/dist/tools/ls.d.ts +21 -0
- package/dist/tools/ls.js +96 -0
- package/dist/tools/output-accumulator.d.ts +48 -0
- package/dist/tools/output-accumulator.js +106 -0
- package/dist/tools/paths.d.ts +20 -0
- package/dist/tools/paths.js +58 -0
- package/dist/tools/presets.d.ts +71 -0
- package/dist/tools/presets.js +128 -0
- package/dist/tools/process-tree.d.ts +35 -0
- package/dist/tools/process-tree.js +125 -0
- package/dist/tools/read.d.ts +28 -0
- package/dist/tools/read.js +181 -0
- package/dist/tools/registry.d.ts +43 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/shell.d.ts +38 -0
- package/dist/tools/shell.js +91 -0
- package/dist/tools/task.d.ts +29 -0
- package/dist/tools/task.js +107 -0
- package/dist/tools/todo.d.ts +28 -0
- package/dist/tools/todo.js +107 -0
- package/dist/tools/truncate.d.ts +44 -0
- package/dist/tools/truncate.js +175 -0
- package/dist/tools/types.d.ts +135 -0
- package/dist/tools/types.js +19 -0
- package/dist/tools/write.d.ts +20 -0
- package/dist/tools/write.js +84 -0
- package/dist/tui/ansi.d.ts +29 -0
- package/dist/tui/ansi.js +420 -0
- package/dist/tui/component.d.ts +38 -0
- package/dist/tui/component.js +13 -0
- package/dist/tui/components/box.d.ts +26 -0
- package/dist/tui/components/box.js +65 -0
- package/dist/tui/components/container.d.ts +14 -0
- package/dist/tui/components/container.js +39 -0
- package/dist/tui/components/editor-buffer.d.ts +67 -0
- package/dist/tui/components/editor-buffer.js +300 -0
- package/dist/tui/components/editor-paste.d.ts +29 -0
- package/dist/tui/components/editor-paste.js +64 -0
- package/dist/tui/components/editor.d.ts +114 -0
- package/dist/tui/components/editor.js +530 -0
- package/dist/tui/components/key-value.d.ts +29 -0
- package/dist/tui/components/key-value.js +48 -0
- package/dist/tui/components/loader.d.ts +35 -0
- package/dist/tui/components/loader.js +71 -0
- package/dist/tui/components/markdown.d.ts +90 -0
- package/dist/tui/components/markdown.js +462 -0
- package/dist/tui/components/meter.d.ts +29 -0
- package/dist/tui/components/meter.js +47 -0
- package/dist/tui/components/overlay.d.ts +24 -0
- package/dist/tui/components/overlay.js +52 -0
- package/dist/tui/components/select-list.d.ts +57 -0
- package/dist/tui/components/select-list.js +179 -0
- package/dist/tui/components/spacer.d.ts +11 -0
- package/dist/tui/components/spacer.js +16 -0
- package/dist/tui/components/text.d.ts +30 -0
- package/dist/tui/components/text.js +65 -0
- package/dist/tui/keybindings.d.ts +66 -0
- package/dist/tui/keybindings.js +120 -0
- package/dist/tui/keys.d.ts +38 -0
- package/dist/tui/keys.js +275 -0
- package/dist/tui/stdin-buffer.d.ts +41 -0
- package/dist/tui/stdin-buffer.js +206 -0
- package/dist/tui/terminal.d.ts +79 -0
- package/dist/tui/terminal.js +153 -0
- package/dist/tui/theme.d.ts +31 -0
- package/dist/tui/theme.js +199 -0
- package/dist/tui/tui.d.ts +103 -0
- package/dist/tui/tui.js +363 -0
- package/dist/tui/vt-screen.d.ts +46 -0
- package/dist/tui/vt-screen.js +251 -0
- package/dist/tui.d.ts +30 -0
- package/dist/tui.js +31 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +24 -0
- package/docs/codemode.md +102 -0
- package/docs/hooks.md +196 -0
- package/docs/host-api.md +161 -0
- package/docs/providers.md +316 -0
- package/docs/rpc.md +253 -0
- package/docs/session-format.md +119 -0
- package/docs/tui.md +168 -0
- package/package.json +101 -4
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook matcher(设计 §6.1「matcher」)。[B5]
|
|
3
|
+
*
|
|
4
|
+
* 语法:缺省 / 空串 / `*` 匹配全部;`bash` 精确;`write|edit` 多选;`canvas_*` glob;
|
|
5
|
+
* `bash(git push*)` 先匹配工具名,再对参数文本做 glob;`/regex/flags` 对工具名做正则。
|
|
6
|
+
* 括号内的文本:bash → `input.command`;文件类工具 → `input.path`(或 `file_path`);
|
|
7
|
+
* 其它工具 → 输入 JSON 文本。`|` 只在括号外分隔候选。只在 Pre/PostToolUse 生效。
|
|
8
|
+
*/
|
|
9
|
+
/** glob → 正则:`*` 任意字符串(含 `/`),`?` 单个字符,其余字面量。 */
|
|
10
|
+
export function globToRegExp(glob) {
|
|
11
|
+
let source = "";
|
|
12
|
+
for (const ch of glob) {
|
|
13
|
+
if (ch === "*")
|
|
14
|
+
source += ".*";
|
|
15
|
+
else if (ch === "?")
|
|
16
|
+
source += ".";
|
|
17
|
+
else
|
|
18
|
+
source += ch.replace(/[\\^$.+()|[\]{}]/g, "\\$&");
|
|
19
|
+
}
|
|
20
|
+
return new RegExp(`^${source}$`, "s");
|
|
21
|
+
}
|
|
22
|
+
/** 括号外的 `|` 切分。 */
|
|
23
|
+
export function splitAlternatives(matcher) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let current = "";
|
|
27
|
+
for (const ch of matcher) {
|
|
28
|
+
if (ch === "(")
|
|
29
|
+
depth++;
|
|
30
|
+
else if (ch === ")")
|
|
31
|
+
depth = Math.max(0, depth - 1);
|
|
32
|
+
if (ch === "|" && depth === 0) {
|
|
33
|
+
parts.push(current);
|
|
34
|
+
current = "";
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
current += ch;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
parts.push(current);
|
|
41
|
+
return parts.map((p) => p.trim()).filter((p) => p !== "");
|
|
42
|
+
}
|
|
43
|
+
/** 括号内 glob 匹配的参数文本。 */
|
|
44
|
+
export function argumentText(toolName, input) {
|
|
45
|
+
if (typeof input === "object" && input !== null) {
|
|
46
|
+
const record = input;
|
|
47
|
+
if (toolName === "bash" && typeof record["command"] === "string")
|
|
48
|
+
return record["command"];
|
|
49
|
+
for (const key of ["path", "file_path", "pattern"]) {
|
|
50
|
+
const value = record[key];
|
|
51
|
+
if (typeof value === "string")
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (typeof input === "string")
|
|
56
|
+
return input;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(input) ?? "";
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const REGEX_FORM = /^\/(.+)\/([a-z]*)$/s;
|
|
65
|
+
const CALL_FORM = /^([^()]+)\((.*)\)$/s;
|
|
66
|
+
function compileAlternative(alt, nameOnly = false) {
|
|
67
|
+
const regex = REGEX_FORM.exec(alt);
|
|
68
|
+
if (regex !== null) {
|
|
69
|
+
const re = new RegExp(regex[1] ?? "", (regex[2] ?? "").replace(/[gy]/g, ""));
|
|
70
|
+
return (toolName) => re.test(toolName);
|
|
71
|
+
}
|
|
72
|
+
const call = CALL_FORM.exec(alt);
|
|
73
|
+
if (call !== null) {
|
|
74
|
+
const name = globToRegExp((call[1] ?? "").trim());
|
|
75
|
+
const arg = globToRegExp((call[2] ?? "").trim());
|
|
76
|
+
if (nameOnly)
|
|
77
|
+
return (toolName) => name.test(toolName);
|
|
78
|
+
return (toolName, input) => name.test(toolName) && arg.test(argumentText(toolName, input));
|
|
79
|
+
}
|
|
80
|
+
const name = globToRegExp(alt);
|
|
81
|
+
return (toolName) => name.test(toolName);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 编译 matcher;语法错误(非法正则)抛 Error。
|
|
85
|
+
* `nameOnly`:忽略括号参数,只看工具名(输入未知时判断「可能匹配」)。
|
|
86
|
+
*/
|
|
87
|
+
export function compileMatcher(matcher, nameOnly = false) {
|
|
88
|
+
if (matcher === undefined)
|
|
89
|
+
return () => true;
|
|
90
|
+
const trimmed = matcher.trim();
|
|
91
|
+
if (trimmed === "" || trimmed === "*")
|
|
92
|
+
return () => true;
|
|
93
|
+
// 整串是正则时不按 `|` 切分(正则里的 `|` 是正则自己的)。
|
|
94
|
+
if (REGEX_FORM.test(trimmed))
|
|
95
|
+
return compileAlternative(trimmed);
|
|
96
|
+
const alternatives = splitAlternatives(trimmed).map((alt) => compileAlternative(alt, nameOnly));
|
|
97
|
+
return (toolName, input) => alternatives.some((m) => m(toolName, input));
|
|
98
|
+
}
|
|
99
|
+
/** 校验 matcher 语法;返回错误信息或 undefined。 */
|
|
100
|
+
export function checkMatcher(matcher) {
|
|
101
|
+
try {
|
|
102
|
+
compileMatcher(matcher);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return error.message;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook 输入 / 输出的构造、校验与决策合并(设计 §6.1)。[B5]
|
|
3
|
+
*
|
|
4
|
+
* 退出码语义:0 放行并解析 stdout;2 阻止(stderr 作为 reason);其它非零 = Hook 自身错误,
|
|
5
|
+
* 非阻塞、记 warning、按「无决策」继续;超时同非阻塞错误,但 PreToolUse 超时按 deny。
|
|
6
|
+
* 合并:决策最严 `deny > block > ask > allow`;`updatedInput` / `updatedPrompt` 只接受唯一
|
|
7
|
+
* 一个返回者(多个则全部忽略并 warning);`additionalContext` / `customInstructions` 按配置顺序拼接。
|
|
8
|
+
*/
|
|
9
|
+
import type { HookDecision, HookEvent, HookInput, HookOutcome, HookOutput, HookRunResult } from "./types.js";
|
|
10
|
+
export declare const DECISION_SEVERITY: Readonly<Record<HookDecision, number>>;
|
|
11
|
+
/** 退出码 2 映射的阻止决策;undefined = 该事件忽略退出码 2。 */
|
|
12
|
+
export declare function blockingDecision(event: HookEvent): HookDecision | undefined;
|
|
13
|
+
export interface ParsedOutput {
|
|
14
|
+
output?: HookOutput;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
/** 解析 stdout:空 / 非 JSON / 非对象 → 无输出;字段类型不对的丢弃并 warning。 */
|
|
18
|
+
export declare function parseHookOutput(stdout: string): ParsedOutput;
|
|
19
|
+
/** 单条结果的有效决策与 reason(已按事件与退出码解释)。 */
|
|
20
|
+
export interface Interpreted {
|
|
21
|
+
decision?: HookDecision;
|
|
22
|
+
reason?: string;
|
|
23
|
+
warning?: string;
|
|
24
|
+
/** 是否采用 stdout 的其它字段(只有退出码 0)。 */
|
|
25
|
+
useOutput: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function interpretResult(result: HookRunResult): Interpreted;
|
|
28
|
+
/** 合并同一事件下全部结果(`results` 已按配置顺序排列)。 */
|
|
29
|
+
export declare function mergeResults(results: readonly HookRunResult[], extraWarnings?: readonly string[]): HookOutcome;
|
|
30
|
+
/** 无任何 Hook 时的空结论。 */
|
|
31
|
+
export declare function emptyOutcome(): HookOutcome;
|
|
32
|
+
/** 给 shell 脚本的环境变量(§6.1)。 */
|
|
33
|
+
export declare function hookEnv(input: HookInput): Record<string, string>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook 输入 / 输出的构造、校验与决策合并(设计 §6.1)。[B5]
|
|
3
|
+
*
|
|
4
|
+
* 退出码语义:0 放行并解析 stdout;2 阻止(stderr 作为 reason);其它非零 = Hook 自身错误,
|
|
5
|
+
* 非阻塞、记 warning、按「无决策」继续;超时同非阻塞错误,但 PreToolUse 超时按 deny。
|
|
6
|
+
* 合并:决策最严 `deny > block > ask > allow`;`updatedInput` / `updatedPrompt` 只接受唯一
|
|
7
|
+
* 一个返回者(多个则全部忽略并 warning);`additionalContext` / `customInstructions` 按配置顺序拼接。
|
|
8
|
+
*/
|
|
9
|
+
export const DECISION_SEVERITY = {
|
|
10
|
+
deny: 4,
|
|
11
|
+
block: 3,
|
|
12
|
+
ask: 2,
|
|
13
|
+
allow: 1,
|
|
14
|
+
};
|
|
15
|
+
/** 各事件接受的决策(其它决策忽略并 warning;deny / block 在两类事件间互换)。 */
|
|
16
|
+
const ACCEPTED = {
|
|
17
|
+
PreToolUse: ["allow", "deny", "ask"],
|
|
18
|
+
PostToolUse: ["block"],
|
|
19
|
+
UserPromptSubmit: ["block"],
|
|
20
|
+
Stop: ["block"],
|
|
21
|
+
SubagentStop: ["block"],
|
|
22
|
+
PreCompact: ["block"],
|
|
23
|
+
SessionStart: ["block"],
|
|
24
|
+
Notification: [],
|
|
25
|
+
SessionEnd: [],
|
|
26
|
+
};
|
|
27
|
+
/** 退出码 2 映射的阻止决策;undefined = 该事件忽略退出码 2。 */
|
|
28
|
+
export function blockingDecision(event) {
|
|
29
|
+
if (event === "PreToolUse")
|
|
30
|
+
return "deny";
|
|
31
|
+
if (event === "Notification" || event === "SessionEnd")
|
|
32
|
+
return undefined;
|
|
33
|
+
return "block";
|
|
34
|
+
}
|
|
35
|
+
/** 解析 stdout:空 / 非 JSON / 非对象 → 无输出;字段类型不对的丢弃并 warning。 */
|
|
36
|
+
export function parseHookOutput(stdout) {
|
|
37
|
+
const text = stdout.trim();
|
|
38
|
+
if (text === "" || (text[0] !== "{" && text[0] !== "["))
|
|
39
|
+
return { warnings: [] };
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { warnings: [] };
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
48
|
+
return { warnings: [] };
|
|
49
|
+
const raw = value;
|
|
50
|
+
const output = {};
|
|
51
|
+
const warnings = [];
|
|
52
|
+
const decision = raw["decision"];
|
|
53
|
+
if (decision !== undefined) {
|
|
54
|
+
if (decision === "allow" || decision === "deny" || decision === "ask" || decision === "block") {
|
|
55
|
+
output.decision = decision;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
warnings.push(`未知 decision:${JSON.stringify(decision)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const key of [
|
|
62
|
+
"reason",
|
|
63
|
+
"updatedPrompt",
|
|
64
|
+
"additionalContext",
|
|
65
|
+
"customInstructions",
|
|
66
|
+
]) {
|
|
67
|
+
const v = raw[key];
|
|
68
|
+
if (v === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
if (typeof v === "string")
|
|
71
|
+
output[key] = v;
|
|
72
|
+
else
|
|
73
|
+
warnings.push(`${key} 应为字符串`);
|
|
74
|
+
}
|
|
75
|
+
if ("updatedInput" in raw)
|
|
76
|
+
output.updatedInput = raw["updatedInput"];
|
|
77
|
+
if (raw["continue"] === false)
|
|
78
|
+
output.continue = false;
|
|
79
|
+
if (raw["suppressOutput"] === true)
|
|
80
|
+
output.suppressOutput = true;
|
|
81
|
+
return { output, warnings };
|
|
82
|
+
}
|
|
83
|
+
function describe(result) {
|
|
84
|
+
return `Hook ${result.event}「${result.command}」`;
|
|
85
|
+
}
|
|
86
|
+
function normalizeDecision(event, decision) {
|
|
87
|
+
const accepted = ACCEPTED[event];
|
|
88
|
+
if (accepted.includes(decision))
|
|
89
|
+
return decision;
|
|
90
|
+
if (decision === "deny" && accepted.includes("block"))
|
|
91
|
+
return "block";
|
|
92
|
+
if (decision === "block" && accepted.includes("deny"))
|
|
93
|
+
return "deny";
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
export function interpretResult(result) {
|
|
97
|
+
const event = result.event;
|
|
98
|
+
if (result.timedOut) {
|
|
99
|
+
const message = `${describe(result)} 超时(${result.durationMs} ms)`;
|
|
100
|
+
if (event === "PreToolUse")
|
|
101
|
+
return { decision: "deny", reason: message, warning: message, useOutput: false };
|
|
102
|
+
return { warning: message, useOutput: false };
|
|
103
|
+
}
|
|
104
|
+
if (result.exitCode === 2) {
|
|
105
|
+
const decision = blockingDecision(event);
|
|
106
|
+
const reason = result.stderr.trim() || `${describe(result)} 以退出码 2 阻止`;
|
|
107
|
+
if (decision === undefined)
|
|
108
|
+
return { useOutput: false };
|
|
109
|
+
return { decision, reason, useOutput: false };
|
|
110
|
+
}
|
|
111
|
+
if (result.exitCode !== 0) {
|
|
112
|
+
const tail = result.stderr.trim().split("\n").slice(-3).join(" | ");
|
|
113
|
+
const code = result.exitCode === null ? "被信号终止" : `退出码 ${result.exitCode}`;
|
|
114
|
+
return {
|
|
115
|
+
warning: `${describe(result)} 失败(${code})${tail === "" ? "" : `:${tail}`}`,
|
|
116
|
+
useOutput: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const output = result.output;
|
|
120
|
+
if (output?.decision === undefined)
|
|
121
|
+
return { useOutput: true };
|
|
122
|
+
const decision = normalizeDecision(event, output.decision);
|
|
123
|
+
if (decision === undefined) {
|
|
124
|
+
return {
|
|
125
|
+
useOutput: true,
|
|
126
|
+
warning: `${describe(result)} 的 decision "${output.decision}" 对 ${event} 无效,已忽略`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const interpreted = { decision, useOutput: true };
|
|
130
|
+
if (output.reason !== undefined)
|
|
131
|
+
interpreted.reason = output.reason;
|
|
132
|
+
return interpreted;
|
|
133
|
+
}
|
|
134
|
+
function joinTexts(values) {
|
|
135
|
+
const parts = values.map((v) => v.trim()).filter((v) => v !== "");
|
|
136
|
+
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
137
|
+
}
|
|
138
|
+
/** 合并同一事件下全部结果(`results` 已按配置顺序排列)。 */
|
|
139
|
+
export function mergeResults(results, extraWarnings = []) {
|
|
140
|
+
const warnings = [...extraWarnings];
|
|
141
|
+
let decision;
|
|
142
|
+
let reason;
|
|
143
|
+
const inputs = [];
|
|
144
|
+
const prompts = [];
|
|
145
|
+
const contexts = [];
|
|
146
|
+
const instructions = [];
|
|
147
|
+
let stop = false;
|
|
148
|
+
let suppressOutput = false;
|
|
149
|
+
for (const result of results) {
|
|
150
|
+
const interpreted = interpretResult(result);
|
|
151
|
+
if (interpreted.warning !== undefined)
|
|
152
|
+
warnings.push(interpreted.warning);
|
|
153
|
+
if (interpreted.decision !== undefined) {
|
|
154
|
+
if (decision === undefined ||
|
|
155
|
+
DECISION_SEVERITY[interpreted.decision] > DECISION_SEVERITY[decision]) {
|
|
156
|
+
decision = interpreted.decision;
|
|
157
|
+
reason = interpreted.reason;
|
|
158
|
+
}
|
|
159
|
+
else if (interpreted.decision === decision && reason === undefined) {
|
|
160
|
+
reason = interpreted.reason;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!interpreted.useOutput || result.output === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
const output = result.output;
|
|
166
|
+
if ("updatedInput" in output)
|
|
167
|
+
inputs.push(output.updatedInput);
|
|
168
|
+
if (output.updatedPrompt !== undefined)
|
|
169
|
+
prompts.push(output.updatedPrompt);
|
|
170
|
+
if (output.additionalContext !== undefined)
|
|
171
|
+
contexts.push(output.additionalContext);
|
|
172
|
+
if (output.customInstructions !== undefined)
|
|
173
|
+
instructions.push(output.customInstructions);
|
|
174
|
+
if (output.continue === false) {
|
|
175
|
+
stop = true;
|
|
176
|
+
if (reason === undefined && output.reason !== undefined)
|
|
177
|
+
reason = output.reason;
|
|
178
|
+
}
|
|
179
|
+
if (output.suppressOutput === true)
|
|
180
|
+
suppressOutput = true;
|
|
181
|
+
}
|
|
182
|
+
const outcome = {
|
|
183
|
+
hasUpdatedInput: false,
|
|
184
|
+
stop,
|
|
185
|
+
suppressOutput,
|
|
186
|
+
results: [...results],
|
|
187
|
+
warnings,
|
|
188
|
+
};
|
|
189
|
+
if (decision !== undefined)
|
|
190
|
+
outcome.decision = decision;
|
|
191
|
+
if (reason !== undefined)
|
|
192
|
+
outcome.reason = reason;
|
|
193
|
+
if (inputs.length === 1) {
|
|
194
|
+
outcome.updatedInput = inputs[0];
|
|
195
|
+
outcome.hasUpdatedInput = true;
|
|
196
|
+
}
|
|
197
|
+
else if (inputs.length > 1) {
|
|
198
|
+
warnings.push(`${inputs.length} 个 Hook 同时返回 updatedInput,全部忽略`);
|
|
199
|
+
}
|
|
200
|
+
if (prompts.length === 1 && prompts[0] !== undefined)
|
|
201
|
+
outcome.updatedPrompt = prompts[0];
|
|
202
|
+
else if (prompts.length > 1)
|
|
203
|
+
warnings.push(`${prompts.length} 个 Hook 同时返回 updatedPrompt,全部忽略`);
|
|
204
|
+
const additionalContext = joinTexts(contexts);
|
|
205
|
+
if (additionalContext !== undefined)
|
|
206
|
+
outcome.additionalContext = additionalContext;
|
|
207
|
+
const customInstructions = joinTexts(instructions);
|
|
208
|
+
if (customInstructions !== undefined)
|
|
209
|
+
outcome.customInstructions = customInstructions;
|
|
210
|
+
return outcome;
|
|
211
|
+
}
|
|
212
|
+
/** 无任何 Hook 时的空结论。 */
|
|
213
|
+
export function emptyOutcome() {
|
|
214
|
+
return { hasUpdatedInput: false, stop: false, suppressOutput: false, results: [], warnings: [] };
|
|
215
|
+
}
|
|
216
|
+
/** 给 shell 脚本的环境变量(§6.1)。 */
|
|
217
|
+
export function hookEnv(input) {
|
|
218
|
+
const env = {
|
|
219
|
+
AMA_HOOK_EVENT: input.hookEventName,
|
|
220
|
+
AMA_SESSION_ID: input.sessionId,
|
|
221
|
+
AMA_CWD: input.cwd,
|
|
222
|
+
};
|
|
223
|
+
if (input.toolName !== undefined)
|
|
224
|
+
env["AMA_TOOL_NAME"] = input.toolName;
|
|
225
|
+
const file = filePathOf(input);
|
|
226
|
+
if (file !== undefined)
|
|
227
|
+
env["AMA_FILE"] = file;
|
|
228
|
+
return env;
|
|
229
|
+
}
|
|
230
|
+
function filePathOf(input) {
|
|
231
|
+
if (input.toolName === undefined || !["write", "edit", "read"].includes(input.toolName)) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
const toolInput = input.toolInput;
|
|
235
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
236
|
+
return undefined;
|
|
237
|
+
const record = toolInput;
|
|
238
|
+
const path = record["path"] ?? record["file_path"];
|
|
239
|
+
return typeof path === "string" ? path : undefined;
|
|
240
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook 子进程执行(设计 §6.1「退出码语义」「并行与顺序」)。[B5]
|
|
3
|
+
*
|
|
4
|
+
* - POSIX:`sh -c <command>`,独立进程组,超时 / abort 时 SIGTERM 整组,1 s 后 SIGKILL。
|
|
5
|
+
* - Windows:Git Bash(`AMA_HOOK_SHELL` 或常见安装位置)→ 否则 `cmd /d /s /c`。
|
|
6
|
+
* - cwd 为会话 cwd;stdin 写完 JSON 即关闭(Hook 不读 stdin 时忽略 EPIPE)。
|
|
7
|
+
* - stdout / stderr 各收集上限 1 MiB,超出截断。
|
|
8
|
+
* - 同一事件的多条 Hook 并行启动,结果按配置顺序返回。
|
|
9
|
+
*/
|
|
10
|
+
import type { LoadedHook } from "./config.js";
|
|
11
|
+
import type { HookInput, HookRunResult } from "./types.js";
|
|
12
|
+
export declare const MAX_OUTPUT_BYTES: number;
|
|
13
|
+
export interface ShellCommand {
|
|
14
|
+
file: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
/** Windows cmd 需要原样传参。 */
|
|
17
|
+
verbatim: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** 选择 shell 并拼装参数。 */
|
|
20
|
+
export declare function shellFor(command: string, platform?: NodeJS.Platform, env?: Readonly<Record<string, string | undefined>>): ShellCommand;
|
|
21
|
+
export interface RunHookOptions {
|
|
22
|
+
cwd: string;
|
|
23
|
+
/** 叠加到 process.env 之上的环境。 */
|
|
24
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
25
|
+
signal?: AbortSignal | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** 运行一条 Hook 命令;不抛错(spawn 失败记为退出码 127 + stderr)。 */
|
|
28
|
+
export declare function runHookCommand(hook: Pick<LoadedHook, "event" | "command" | "timeoutMs" | "source">, input: HookInput, options: RunHookOptions): Promise<HookRunResult>;
|
|
29
|
+
/** 并行运行,结果按入参顺序返回。 */
|
|
30
|
+
export declare function runHooks(hooks: readonly Pick<LoadedHook, "event" | "command" | "timeoutMs" | "source">[], input: HookInput, options: RunHookOptions): Promise<HookRunResult[]>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook 子进程执行(设计 §6.1「退出码语义」「并行与顺序」)。[B5]
|
|
3
|
+
*
|
|
4
|
+
* - POSIX:`sh -c <command>`,独立进程组,超时 / abort 时 SIGTERM 整组,1 s 后 SIGKILL。
|
|
5
|
+
* - Windows:Git Bash(`AMA_HOOK_SHELL` 或常见安装位置)→ 否则 `cmd /d /s /c`。
|
|
6
|
+
* - cwd 为会话 cwd;stdin 写完 JSON 即关闭(Hook 不读 stdin 时忽略 EPIPE)。
|
|
7
|
+
* - stdout / stderr 各收集上限 1 MiB,超出截断。
|
|
8
|
+
* - 同一事件的多条 Hook 并行启动,结果按配置顺序返回。
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { hookEnv, parseHookOutput } from "./protocol.js";
|
|
14
|
+
export const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
15
|
+
const KILL_GRACE_MS = 1_000;
|
|
16
|
+
/** 选择 shell 并拼装参数。 */
|
|
17
|
+
export function shellFor(command, platform = process.platform, env = process.env) {
|
|
18
|
+
if (platform !== "win32")
|
|
19
|
+
return { file: "/bin/sh", args: ["-c", command], verbatim: false };
|
|
20
|
+
const candidates = [
|
|
21
|
+
env["AMA_HOOK_SHELL"],
|
|
22
|
+
env["ProgramFiles"] && join(env["ProgramFiles"], "Git", "bin", "bash.exe"),
|
|
23
|
+
env["ProgramW6432"] && join(env["ProgramW6432"], "Git", "bin", "bash.exe"),
|
|
24
|
+
];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (candidate && existsSync(candidate))
|
|
27
|
+
return { file: candidate, args: ["-c", command], verbatim: false };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
file: env["ComSpec"] ?? "cmd.exe",
|
|
31
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
32
|
+
verbatim: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
class Collector {
|
|
36
|
+
chunks = [];
|
|
37
|
+
size = 0;
|
|
38
|
+
truncated = false;
|
|
39
|
+
push(chunk) {
|
|
40
|
+
if (this.size >= MAX_OUTPUT_BYTES) {
|
|
41
|
+
this.truncated = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const room = MAX_OUTPUT_BYTES - this.size;
|
|
45
|
+
const part = chunk.length > room ? chunk.subarray(0, room) : chunk;
|
|
46
|
+
if (part.length < chunk.length)
|
|
47
|
+
this.truncated = true;
|
|
48
|
+
this.chunks.push(part);
|
|
49
|
+
this.size += part.length;
|
|
50
|
+
}
|
|
51
|
+
text() {
|
|
52
|
+
return Buffer.concat(this.chunks).toString("utf8");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function killTree(pid, signal) {
|
|
56
|
+
if (pid === undefined)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
if (process.platform === "win32") {
|
|
60
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
process.kill(-pid, signal);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, signal);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// 进程已退出
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** 运行一条 Hook 命令;不抛错(spawn 失败记为退出码 127 + stderr)。 */
|
|
76
|
+
export function runHookCommand(hook, input, options) {
|
|
77
|
+
const started = Date.now();
|
|
78
|
+
const shell = shellFor(hook.command);
|
|
79
|
+
return new Promise((resolveResult) => {
|
|
80
|
+
const stdout = new Collector();
|
|
81
|
+
const stderr = new Collector();
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
let settled = false;
|
|
84
|
+
const child = spawn(shell.file, shell.args, {
|
|
85
|
+
cwd: options.cwd,
|
|
86
|
+
env: { ...process.env, ...options.env, ...hookEnv(input) },
|
|
87
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
88
|
+
detached: process.platform !== "win32",
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
windowsVerbatimArguments: shell.verbatim,
|
|
91
|
+
});
|
|
92
|
+
let killTimer;
|
|
93
|
+
const terminate = () => {
|
|
94
|
+
killTree(child.pid, "SIGTERM");
|
|
95
|
+
killTimer = setTimeout(() => killTree(child.pid, "SIGKILL"), KILL_GRACE_MS);
|
|
96
|
+
killTimer.unref();
|
|
97
|
+
};
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
timedOut = true;
|
|
100
|
+
terminate();
|
|
101
|
+
}, hook.timeoutMs);
|
|
102
|
+
const onAbort = () => terminate();
|
|
103
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
104
|
+
const finish = (exitCode, extraStderr) => {
|
|
105
|
+
if (settled)
|
|
106
|
+
return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
if (killTimer !== undefined)
|
|
110
|
+
clearTimeout(killTimer);
|
|
111
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
112
|
+
const out = stdout.text();
|
|
113
|
+
const result = {
|
|
114
|
+
event: hook.event,
|
|
115
|
+
command: hook.command,
|
|
116
|
+
source: hook.source,
|
|
117
|
+
exitCode: timedOut ? null : exitCode,
|
|
118
|
+
timedOut,
|
|
119
|
+
durationMs: Date.now() - started,
|
|
120
|
+
stdout: out,
|
|
121
|
+
stderr: stderr.text() + (extraStderr ?? ""),
|
|
122
|
+
};
|
|
123
|
+
if (!timedOut && exitCode === 0) {
|
|
124
|
+
const parsed = parseHookOutput(out);
|
|
125
|
+
if (parsed.output !== undefined)
|
|
126
|
+
result.output = parsed.output;
|
|
127
|
+
}
|
|
128
|
+
resolveResult(result);
|
|
129
|
+
};
|
|
130
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
131
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
132
|
+
child.on("error", (error) => finish(127, `${error.message}\n`));
|
|
133
|
+
child.on("close", (code) => finish(code));
|
|
134
|
+
child.stdin.on("error", () => {
|
|
135
|
+
// Hook 没读 stdin 就退出:EPIPE 无害。
|
|
136
|
+
});
|
|
137
|
+
child.stdin.end(`${JSON.stringify(input)}\n`);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** 并行运行,结果按入参顺序返回。 */
|
|
141
|
+
export function runHooks(hooks, input, options) {
|
|
142
|
+
return Promise.all(hooks.map((hook) => runHookCommand(hook, input, options)));
|
|
143
|
+
}
|