@shelken/pi-co-authored-by 0.2.8 → 0.2.10

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/README.md CHANGED
@@ -7,13 +7,14 @@ Co-Authored-By: <model> <noreply@pi.dev>
7
7
  Generated-By: pi <version>
8
8
  ```
9
9
 
10
- 实现:session 级临时 `core.hooksPath` + `prepare-commit-msg`;结束时清理。
10
+ 实现:识别 bash 命令中直接执行的 `git commit`(支持常见前缀与组合,脚本内间接 commit 不算),将命令交给外部 runner 在真实 hooks 目录临时安装 `prepare-commit-msg`(不改 `core.hooksPath`);pid 引用计数,shell 退出时最后一个引用卸装并恢复原 hook。非 commit 命令原样放行,不影响其他扩展。
11
11
 
12
12
  初始 fork 自 [bruno-garcia/pi-co-authored-by](https://github.com/bruno-garcia/pi-co-authored-by)。
13
13
 
14
14
  ## 功能
15
15
 
16
16
  - 只挂钩 `bash` 工具调用
17
+ - 只处理直接执行的 `git commit`;`printf`、`git status`、脚本内 commit 等一律不改
17
18
  - 模型名取当前 session;没有则 `unknown`
18
19
  - 与仓库已有 hook 兼容(先跑临时 hook,再链式原 hook)
19
20
 
package/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  removeCommitHookDirectory,
6
6
  wrapBashWithCommitHook,
7
7
  } from "./lib/commit.ts";
8
+ import { isGitCommitCommand } from "./lib/git-commit.ts";
8
9
 
9
10
  export default function (pi: ExtensionAPI) {
10
11
  let hooksDir: string | undefined;
@@ -25,6 +26,8 @@ export default function (pi: ExtensionAPI) {
25
26
 
26
27
  pi.on("tool_call", async (event, ctx) => {
27
28
  if (!isToolCallEventType("bash", event)) return;
29
+ // 只包装直接执行的 git commit;其他命令原样放行,避免影响其他扩展
30
+ if (!isGitCommitCommand(event.input.command)) return;
28
31
 
29
32
  const model = ctx.model;
30
33
  const modelName = model ? (model.name || `${model.provider}/${model.id}`) : "unknown";
package/lib/commit.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -8,9 +8,10 @@ export const GENERATED_BY_NAME = "Generated-By";
8
8
  /** Create a session-scoped Git hooks directory for commit trailer injection. */
9
9
  export function createCommitHookDirectory(): string {
10
10
  const hooksDir = mkdtempSync(join(tmpdir(), "pi-co-authored-by-hooks-"));
11
- const hookPath = join(hooksDir, "prepare-commit-msg");
12
- writeFileSync(hookPath, buildPrepareCommitMsgHook(), { mode: 0o755 });
13
- chmodSync(hookPath, 0o755);
11
+ writeFileSync(join(hooksDir, "prepare-commit-msg"), buildPrepareCommitMsgHook(), {
12
+ mode: 0o755,
13
+ });
14
+ writeFileSync(join(hooksDir, "run"), buildRunnerScript(), { mode: 0o755 });
14
15
  return hooksDir;
15
16
  }
16
17
 
@@ -20,7 +21,10 @@ export function removeCommitHookDirectory(hooksDir: string | undefined): void {
20
21
  rmSync(hooksDir, { recursive: true, force: true });
21
22
  }
22
23
 
23
- /** Add process-local Git hook configuration and trailer metadata to a bash command. */
24
+ /**
25
+ * 用外部 runner 包裹 git commit:其他改写器只能看到不可拆的 wrapper 调用,
26
+ * 不会二次解析内部 shell;元数据经环境变量传入,命令作为单个引号参数。
27
+ */
24
28
  export function wrapBashWithCommitHook(
25
29
  cmd: string,
26
30
  hooksDir: string,
@@ -29,33 +33,142 @@ export function wrapBashWithCommitHook(
29
33
  ): string {
30
34
  const coAuthor = `${CO_AUTHOR_NAME}: ${modelName} <noreply@pi.dev>`;
31
35
  const generatedBy = `${GENERATED_BY_NAME}: pi ${piVersion}`;
36
+ const runPath = join(hooksDir, "run");
32
37
 
33
- return `${buildEnvironmentPrefix(hooksDir, coAuthor, generatedBy)}\n${cmd}`;
38
+ return [
39
+ `PI_CO_AUTHORED_BY_CO_AUTHOR=${shellQuote(coAuthor)}`,
40
+ `PI_CO_AUTHORED_BY_GENERATED_BY=${shellQuote(generatedBy)}`,
41
+ shellQuote(runPath),
42
+ shellQuote(cmd),
43
+ ].join(" ");
34
44
  }
35
45
 
36
- function buildEnvironmentPrefix(
37
- hooksDir: string,
38
- coAuthor: string,
39
- generatedBy: string,
40
- ): string {
41
- return `__pi_co_authored_by_git_config_index="\${GIT_CONFIG_COUNT:-0}"
42
- export PI_CO_AUTHORED_BY_GIT_CONFIG_INDEX="$__pi_co_authored_by_git_config_index"
43
- export PI_CO_AUTHORED_BY_CO_AUTHOR=${shellQuote(coAuthor)}
44
- export PI_CO_AUTHORED_BY_GENERATED_BY=${shellQuote(generatedBy)}
45
- export "GIT_CONFIG_KEY_\${__pi_co_authored_by_git_config_index}=core.hooksPath"
46
- export "GIT_CONFIG_VALUE_\${__pi_co_authored_by_git_config_index}=${escapeDoubleQuotedAssignmentValue(hooksDir)}"
47
- export GIT_CONFIG_COUNT="$((__pi_co_authored_by_git_config_index + 1))"
48
- unset __pi_co_authored_by_git_config_index`;
46
+ function buildRunnerScript(): string {
47
+ return `#!/bin/bash
48
+ # PI_CO_AUTHORED_BY_RUNNER
49
+ export PI_CO_AUTHORED_BY_HOOK_SRC="$(dirname "$0")/prepare-commit-msg"
50
+
51
+ __pi_co_authored_by_installed_hooks=""
52
+
53
+ __pi_co_authored_by_resolve_hooks_dir() {
54
+ local configured hooks_dir toplevel
55
+ configured="$(command git config --get core.hooksPath 2>/dev/null || true)"
56
+ if [ -n "$configured" ]; then
57
+ case "$configured" in
58
+ /*) hooks_dir="$configured" ;;
59
+ *)
60
+ toplevel="$(command git rev-parse --show-toplevel 2>/dev/null)" || return 1
61
+ hooks_dir="$toplevel/$configured"
62
+ ;;
63
+ esac
64
+ else
65
+ hooks_dir="$(command git rev-parse --git-path hooks 2>/dev/null)" || return 1
66
+ fi
67
+ # 绝对路径,避免 cwd 变化后 cleanup 找不到
68
+ case "$hooks_dir" in
69
+ /*) ;;
70
+ *) hooks_dir="$(CDPATH= cd -- "$hooks_dir" 2>/dev/null && pwd)" || return 1 ;;
71
+ esac
72
+ printf '%s' "$hooks_dir"
73
+ }
74
+
75
+ # 清掉已死进程的 ref,避免 SIGKILL 后永远占坑
76
+ __pi_co_authored_by_prune_refs() {
77
+ local refs_dir="$1" f pid
78
+ [ -d "$refs_dir" ] || return 0
79
+ for f in "$refs_dir"/*; do
80
+ [ -e "$f" ] || continue
81
+ pid="\${f##*/}"
82
+ case "$pid" in
83
+ ''|*[!0-9]*) rm -f "$f" ;;
84
+ *) kill -0 "$pid" 2>/dev/null || rm -f "$f" ;;
85
+ esac
86
+ done
87
+ }
88
+
89
+ # 登记本 shell 对 hooks_dir 的引用(见 marker 也要登记,否则 sticky dirty)
90
+ __pi_co_authored_by_register() {
91
+ local hooks_dir="$1" refs_dir
92
+ refs_dir="$hooks_dir/.pi-co-authored-by-refs"
93
+ mkdir -p "$refs_dir" 2>/dev/null || return 0
94
+ __pi_co_authored_by_prune_refs "$refs_dir"
95
+ : > "$refs_dir/$$" 2>/dev/null || return 0
96
+ case "
97
+ $__pi_co_authored_by_installed_hooks
98
+ " in
99
+ *"
100
+ $hooks_dir
101
+ "*) ;;
102
+ *)
103
+ __pi_co_authored_by_installed_hooks="$__pi_co_authored_by_installed_hooks
104
+ $hooks_dir"
105
+ ;;
106
+ esac
107
+ }
108
+
109
+ __pi_co_authored_by_ensure_hook() {
110
+ local hooks_dir hook
111
+ hooks_dir="$(__pi_co_authored_by_resolve_hooks_dir)" || return 0
112
+ hook="$hooks_dir/prepare-commit-msg"
113
+ mkdir -p "$hooks_dir" 2>/dev/null || return 0
114
+
115
+ if [ -f "$hook" ] && grep -q 'PI_CO_AUTHORED_BY_HOOK_MARKER' "$hook" 2>/dev/null; then
116
+ __pi_co_authored_by_register "$hooks_dir"
117
+ return 0
118
+ fi
119
+
120
+ if [ -e "$hook" ] && [ ! -e "$hooks_dir/prepare-commit-msg.pi-user" ]; then
121
+ mv "$hook" "$hooks_dir/prepare-commit-msg.pi-user"
122
+ fi
123
+
124
+ cp "$PI_CO_AUTHORED_BY_HOOK_SRC" "$hook"
125
+ chmod +x "$hook"
126
+ __pi_co_authored_by_register "$hooks_dir"
127
+ }
128
+
129
+ __pi_co_authored_by_cleanup() {
130
+ local hooks_dir hook refs_dir
131
+ printf '%s\n' "$__pi_co_authored_by_installed_hooks" | while IFS= read -r hooks_dir; do
132
+ [ -n "$hooks_dir" ] || continue
133
+ refs_dir="$hooks_dir/.pi-co-authored-by-refs"
134
+ rm -f "$refs_dir/$$" 2>/dev/null || true
135
+ __pi_co_authored_by_prune_refs "$refs_dir"
136
+ # 仍有活进程占用:只退自己的 ref,不卸 hook
137
+ if [ -n "$(ls -A "$refs_dir" 2>/dev/null)" ]; then
138
+ continue
139
+ fi
140
+ rmdir "$refs_dir" 2>/dev/null || true
141
+ hook="$hooks_dir/prepare-commit-msg"
142
+ if [ -f "$hook" ] && grep -q 'PI_CO_AUTHORED_BY_HOOK_MARKER' "$hook" 2>/dev/null; then
143
+ rm -f "$hook"
144
+ if [ -e "$hooks_dir/prepare-commit-msg.pi-user" ]; then
145
+ mv "$hooks_dir/prepare-commit-msg.pi-user" "$hook"
146
+ fi
147
+ fi
148
+ done
149
+ }
150
+
151
+ trap '__pi_co_authored_by_cleanup' EXIT
152
+ __pi_co_authored_by_ensure_hook
153
+
154
+ git() {
155
+ __pi_co_authored_by_ensure_hook
156
+ command git "$@"
157
+ }
158
+
159
+ eval "$1"
160
+ `;
49
161
  }
50
162
 
51
163
  function buildPrepareCommitMsgHook(): string {
52
164
  return `#!/bin/sh
165
+ # PI_CO_AUTHORED_BY_HOOK_MARKER
53
166
  set -u
54
167
 
55
168
  message_file="$1"
56
169
 
57
170
  if [ -n "\${PI_CO_AUTHORED_BY_CO_AUTHOR:-}" ] && [ -n "\${PI_CO_AUTHORED_BY_GENERATED_BY:-}" ]; then
58
- git \
171
+ command git \
59
172
  -c trailer.co-authored-by.ifExists=addIfDifferent \
60
173
  -c trailer.generated-by.ifExists=replace \
61
174
  interpret-trailers \
@@ -65,25 +178,10 @@ if [ -n "\${PI_CO_AUTHORED_BY_CO_AUTHOR:-}" ] && [ -n "\${PI_CO_AUTHORED_BY_GENE
65
178
  "$message_file"
66
179
  fi
67
180
 
68
- if [ -n "\${PI_CO_AUTHORED_BY_GIT_CONFIG_INDEX:-}" ]; then
69
- __pi_config_index="$PI_CO_AUTHORED_BY_GIT_CONFIG_INDEX"
70
- unset "GIT_CONFIG_KEY_$__pi_config_index"
71
- unset "GIT_CONFIG_VALUE_$__pi_config_index"
72
- export GIT_CONFIG_COUNT="$__pi_config_index"
73
- fi
74
-
75
- original_hooks_path="$(git config --get core.hooksPath || true)"
76
- if [ -n "$original_hooks_path" ]; then
77
- case "$original_hooks_path" in
78
- /*) original_hook="$original_hooks_path/prepare-commit-msg" ;;
79
- *) original_hook="$(git rev-parse --show-toplevel)/$original_hooks_path/prepare-commit-msg" ;;
80
- esac
81
- else
82
- original_hook="$(git rev-parse --git-path hooks/prepare-commit-msg)"
83
- fi
84
-
85
- if [ -x "$original_hook" ] && [ "$original_hook" != "$0" ]; then
86
- "$original_hook" "$@"
181
+ hooks_dir="$(dirname "$0")"
182
+ user_hook="$hooks_dir/prepare-commit-msg.pi-user"
183
+ if [ -x "$user_hook" ]; then
184
+ "$user_hook" "$@"
87
185
  fi
88
186
  `;
89
187
  }
@@ -91,7 +189,3 @@ fi
91
189
  function shellQuote(value: string): string {
92
190
  return `'${value.replace(/'/g, `'\\''`)}'`;
93
191
  }
94
-
95
- function escapeDoubleQuotedAssignmentValue(value: string): string {
96
- return value.replace(/[\\"$`]/g, "\\$&");
97
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shelken/pi-co-authored-by",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "agent 执行 git commit 时自动追加 Co-Authored-By / Generated-By trailer",
5
5
  "type": "module",
6
6
  "license": "MIT",