@rachel_rotenberg/ai-contribution-tracker 1.0.20 → 1.0.22

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.
Files changed (3) hide show
  1. package/cli.js +27 -30
  2. package/index.ts +1 -0
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -19,7 +19,8 @@ const { execSync, execFileSync } = require("child_process");
19
19
 
20
20
  // ─── Constants ──────────────────────────────────────────────
21
21
  const PLUGIN_NAME = "@rachel_rotenberg/ai-contribution-tracker";
22
- const HOOK_MARKER = "AI_IMPACT_PENDING";
22
+ const HOOK_BEGIN = "# BEGIN ai-contribution-tracker-cli";
23
+ const HOOK_END = "# END ai-contribution-tracker-cli";
23
24
 
24
25
  // ─── Logging helpers ────────────────────────────────────────
25
26
  function ok(msg) { console.log(` \u2713 ${msg}`); }
@@ -29,20 +30,22 @@ function fail(msg) { console.error(` \u2717 ${msg}`); }
29
30
 
30
31
  // ─── Git hook body (shell script, runs on all platforms via Git Bash) ───
31
32
  const HOOK_BODY = [
32
- "",
33
- "# AI Contribution Tracker \u2014 reads AI_IMPACT_PENDING flag",
33
+ "# BEGIN ai-contribution-tracker-cli",
34
34
  'IMPACT_FLAG=$(git rev-parse --git-path AI_IMPACT_PENDING)',
35
35
  'STATE_FILE=$(git rev-parse --git-path ai-tracker-state.json)',
36
36
  'if [ -f "$IMPACT_FLAG" ]; then',
37
37
  ' MARKER=$(cat "$IMPACT_FLAG")',
38
38
  ' if [ -z "$MARKER" ]; then MARKER="Impacted by AI"; fi',
39
- ' if ! grep -qF "$MARKER" "$1"; then',
39
+ ' FIRST_LINE=$(head -n 1 "$IMPACT_FLAG")',
40
+ ' if ! grep -qF "$FIRST_LINE" "$1"; then',
41
+ ' if [ -s "$1" ] && [ "$(tail -c 1 "$1" | wc -l)" -eq 0 ]; then printf "\\n" >> "$1"; fi',
40
42
  ' echo "" >> "$1"',
41
43
  ' echo "$MARKER" >> "$1"',
42
44
  ' fi',
43
45
  ' rm "$IMPACT_FLAG"',
44
46
  'fi',
45
47
  'if [ -f "$STATE_FILE" ]; then rm "$STATE_FILE"; fi',
48
+ "# END ai-contribution-tracker-cli",
46
49
  ].join("\n");
47
50
 
48
51
  // ─── Git hook installation ──────────────────────────────────
@@ -53,14 +56,22 @@ function appendOrCreateHook(hooksDir) {
53
56
 
54
57
  if (fs.existsSync(hookPath)) {
55
58
  const existing = fs.readFileSync(hookPath, "utf8");
56
- if (existing.includes(HOOK_MARKER)) {
59
+ if (existing.includes(HOOK_BEGIN)) {
57
60
  skip(`commit-msg hook already has AI tracker snippet: ${hookPath}`);
58
61
  return;
59
62
  }
60
63
  fs.appendFileSync(hookPath, "\n" + HOOK_BODY + "\n");
61
64
  ok(`Appended AI tracker snippet to existing hook: ${hookPath}`);
62
65
  } else {
63
- const content = ("#!/bin/sh\n" + HOOK_BODY + "\n").replace(/\r\n/g, "\n");
66
+ const delegation = [
67
+ "#!/bin/sh",
68
+ 'LOCAL_HOOK="$(git rev-parse --git-dir)/hooks/commit-msg"',
69
+ 'if [ -f "$LOCAL_HOOK" ] && [ -x "$LOCAL_HOOK" ]; then',
70
+ ' "$LOCAL_HOOK" "$@" || exit $?',
71
+ 'fi',
72
+ "",
73
+ ].join("\n");
74
+ const content = (delegation + HOOK_BODY + "\n").replace(/\r\n/g, "\n");
64
75
  fs.writeFileSync(hookPath, content);
65
76
  ok(`Created commit-msg hook: ${hookPath}`);
66
77
  }
@@ -133,7 +144,7 @@ function addPluginToConfig(configDir) {
133
144
 
134
145
  // Check if already registered (plain string or [name, options] tuple)
135
146
  const alreadyRegistered = plugins.some(
136
- (p) => (typeof p === "string" ? p : p[0]) === PLUGIN_NAME
147
+ (p) => p && (typeof p === "string" ? p : Array.isArray(p) ? p[0] : null) === PLUGIN_NAME
137
148
  );
138
149
 
139
150
  if (alreadyRegistered) {
@@ -172,7 +183,7 @@ function showStatus() {
172
183
 
173
184
  if (hooksPath) {
174
185
  const hookFile = path.join(hooksPath, "commit-msg");
175
- if (fs.existsSync(hookFile) && fs.readFileSync(hookFile, "utf8").includes(HOOK_MARKER)) {
186
+ if (fs.existsSync(hookFile) && fs.readFileSync(hookFile, "utf8").includes(HOOK_BEGIN)) {
176
187
  ok(`Git hook installed: ${hookFile}`);
177
188
  } else {
178
189
  warn(`core.hooksPath set to ${hooksPath} but no AI tracker snippet found`);
@@ -188,7 +199,7 @@ function showStatus() {
188
199
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
189
200
  const plugins = Array.isArray(config.plugin) ? config.plugin : [];
190
201
  const found = plugins.some(
191
- (p) => (typeof p === "string" ? p : p[0]) === PLUGIN_NAME
202
+ (p) => p && (typeof p === "string" ? p : Array.isArray(p) ? p[0] : null) === PLUGIN_NAME
192
203
  );
193
204
  if (found) {
194
205
  ok(`OpenCode plugin registered: ${configPath}`);
@@ -228,8 +239,8 @@ function removeGitHook() {
228
239
  }
229
240
 
230
241
  const content = fs.readFileSync(hookFile, "utf8");
231
- if (!content.includes(HOOK_MARKER)) {
232
- skip("commit-msg hook does not contain AI tracker snippet");
242
+ if (!content.includes(HOOK_BEGIN)) {
243
+ skip("commit-msg hook does not contain AI tracker CLI snippet");
233
244
  return;
234
245
  }
235
246
 
@@ -237,24 +248,10 @@ function removeGitHook() {
237
248
  const filtered = [];
238
249
  let skipping = false;
239
250
 
240
- for (let i = 0; i < lines.length; i++) {
241
- if (lines[i].includes("AI Contribution Tracker")) {
242
- skipping = true;
243
- continue;
244
- }
245
- if (skipping) {
246
- if (lines[i].includes('rm "$STATE_FILE"')) {
247
- // consume the closing `fi` on the next non-empty line, then stop skipping
248
- for (let j = i + 1; j < lines.length; j++) {
249
- if (lines[j].trim() === "fi") { i = j; break; }
250
- if (lines[j].trim() !== "") { i = j - 1; break; }
251
- }
252
- skipping = false;
253
- continue;
254
- }
255
- continue;
256
- }
257
- filtered.push(lines[i]);
251
+ for (const line of lines) {
252
+ if (line.includes(HOOK_BEGIN)) { skipping = true; continue; }
253
+ if (line.includes(HOOK_END)) { skipping = false; continue; }
254
+ if (!skipping) filtered.push(line);
258
255
  }
259
256
 
260
257
  const remaining = filtered.join("\n").trim();
@@ -298,7 +295,7 @@ function removeOpenCodePlugin() {
298
295
 
299
296
  const before = config.plugin.length;
300
297
  config.plugin = config.plugin.filter(
301
- (p) => (typeof p === "string" ? p : p[0]) !== PLUGIN_NAME
298
+ (p) => !p || (typeof p === "string" ? p : Array.isArray(p) ? p[0] : null) !== PLUGIN_NAME
302
299
  );
303
300
 
304
301
  if (config.plugin.length === before) {
package/index.ts CHANGED
@@ -306,3 +306,4 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
306
306
  };
307
307
  export default AIContributionTracker;
308
308
  // Named exports omitted — OpenCode calls all exported functions as plugins
309
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rachel_rotenberg/ai-contribution-tracker",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "OpenCode plugin — tracks AI coding sessions and tags git commits with Impacted by AI markers",
5
5
  "main": "index.ts",
6
6
  "types": "index.ts",