@rachel_rotenberg/ai-contribution-tracker 1.0.19 → 1.0.21

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 +34 -31
  2. package/index.ts +2 -1
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -15,11 +15,12 @@
15
15
  const fs = require("fs");
16
16
  const path = require("path");
17
17
  const os = require("os");
18
- const { execSync } = require("child_process");
18
+ 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 "Impacted by AI" "$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,7 +56,7 @@ 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
  }
@@ -95,7 +98,7 @@ function installGitHook() {
95
98
 
96
99
  try {
97
100
  const gitPath = hooksDir.replace(/\\/g, "/");
98
- execSync(`git config --global core.hooksPath "${gitPath}"`, {
101
+ execFileSync("git", ["config", "--global", "core.hooksPath", gitPath], {
99
102
  encoding: "utf8",
100
103
  stdio: ["pipe", "pipe", "pipe"],
101
104
  });
@@ -133,7 +136,7 @@ function addPluginToConfig(configDir) {
133
136
 
134
137
  // Check if already registered (plain string or [name, options] tuple)
135
138
  const alreadyRegistered = plugins.some(
136
- (p) => (typeof p === "string" ? p : p[0]) === PLUGIN_NAME
139
+ (p) => p && (typeof p === "string" ? p : Array.isArray(p) ? p[0] : null) === PLUGIN_NAME
137
140
  );
138
141
 
139
142
  if (alreadyRegistered) {
@@ -172,7 +175,7 @@ function showStatus() {
172
175
 
173
176
  if (hooksPath) {
174
177
  const hookFile = path.join(hooksPath, "commit-msg");
175
- if (fs.existsSync(hookFile) && fs.readFileSync(hookFile, "utf8").includes(HOOK_MARKER)) {
178
+ if (fs.existsSync(hookFile) && fs.readFileSync(hookFile, "utf8").includes(HOOK_BEGIN)) {
176
179
  ok(`Git hook installed: ${hookFile}`);
177
180
  } else {
178
181
  warn(`core.hooksPath set to ${hooksPath} but no AI tracker snippet found`);
@@ -188,7 +191,7 @@ function showStatus() {
188
191
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
189
192
  const plugins = Array.isArray(config.plugin) ? config.plugin : [];
190
193
  const found = plugins.some(
191
- (p) => (typeof p === "string" ? p : p[0]) === PLUGIN_NAME
194
+ (p) => p && (typeof p === "string" ? p : Array.isArray(p) ? p[0] : null) === PLUGIN_NAME
192
195
  );
193
196
  if (found) {
194
197
  ok(`OpenCode plugin registered: ${configPath}`);
@@ -228,8 +231,8 @@ function removeGitHook() {
228
231
  }
229
232
 
230
233
  const content = fs.readFileSync(hookFile, "utf8");
231
- if (!content.includes(HOOK_MARKER)) {
232
- skip("commit-msg hook does not contain AI tracker snippet");
234
+ if (!content.includes(HOOK_BEGIN)) {
235
+ skip("commit-msg hook does not contain AI tracker CLI snippet");
233
236
  return;
234
237
  }
235
238
 
@@ -237,30 +240,29 @@ function removeGitHook() {
237
240
  const filtered = [];
238
241
  let skipping = false;
239
242
 
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]);
243
+ for (const line of lines) {
244
+ if (line.includes(HOOK_BEGIN)) { skipping = true; continue; }
245
+ if (line.includes(HOOK_END)) { skipping = false; continue; }
246
+ if (!skipping) filtered.push(line);
258
247
  }
259
248
 
260
249
  const remaining = filtered.join("\n").trim();
261
250
  if (remaining === "#!/bin/sh" || remaining === "") {
262
251
  fs.unlinkSync(hookFile);
263
252
  ok(`Removed commit-msg hook: ${hookFile}`);
253
+
254
+ const ourDefaultDir = path.join(os.homedir(), ".config", "ai-contribution-tracker", "git-hooks");
255
+ const normalizedHooksPath = hooksPath.replace(/\\/g, "/");
256
+ const normalizedOurDir = ourDefaultDir.replace(/\\/g, "/");
257
+ if (normalizedHooksPath === normalizedOurDir) {
258
+ try {
259
+ execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"], {
260
+ encoding: "utf8",
261
+ stdio: ["pipe", "pipe", "pipe"],
262
+ });
263
+ ok("Unset git global core.hooksPath");
264
+ } catch { }
265
+ }
264
266
  } else {
265
267
  fs.writeFileSync(hookFile, remaining + "\n");
266
268
  ok(`Removed AI tracker snippet from: ${hookFile}`);
@@ -285,7 +287,7 @@ function removeOpenCodePlugin() {
285
287
 
286
288
  const before = config.plugin.length;
287
289
  config.plugin = config.plugin.filter(
288
- (p) => (typeof p === "string" ? p : p[0]) !== PLUGIN_NAME
290
+ (p) => !p || (typeof p === "string" ? p : Array.isArray(p) ? p[0] : null) !== PLUGIN_NAME
289
291
  );
290
292
 
291
293
  if (config.plugin.length === before) {
@@ -358,3 +360,4 @@ switch (command) {
358
360
  printUsage();
359
361
  process.exit(command ? 1 : 0);
360
362
  }
363
+
package/index.ts CHANGED
@@ -278,7 +278,7 @@ const AIContributionTracker: Plugin = async ({ directory, worktree }) => {
278
278
  if (sess.isSubagent) return;
279
279
 
280
280
  // Resolve gitDir from file path — this is the ONLY place we resolve
281
- if (!sess.gitDir) {
281
+ if (!sess.gitDir || !fs.existsSync(sess.gitDir)) {
282
282
  const args = input.args as Record<string, unknown> ?? {};
283
283
  const fp = typeof args.filePath === "string" ? args.filePath : typeof args.path === "string" ? args.path : undefined;
284
284
  if (fp) {
@@ -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.19",
3
+ "version": "1.0.21",
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",