@rachel_rotenberg/ai-contribution-tracker 1.0.18 → 1.0.20
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/cli.js +374 -0
- package/index.ts +1 -1
- package/package.json +9 -5
package/cli.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AI Contribution Tracker — Standalone CLI Installer
|
|
4
|
+
*
|
|
5
|
+
* Installs the git commit-msg hook and registers the OpenCode plugin
|
|
6
|
+
* without requiring VS Code. Works on macOS, Linux, and Windows.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @rachel_rotenberg/ai-contribution-tracker init # Install everything
|
|
10
|
+
* npx @rachel_rotenberg/ai-contribution-tracker status # Show what's installed
|
|
11
|
+
* npx @rachel_rotenberg/ai-contribution-tracker remove # Uninstall hooks + plugin
|
|
12
|
+
*/
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const os = require("os");
|
|
18
|
+
const { execSync, execFileSync } = require("child_process");
|
|
19
|
+
|
|
20
|
+
// ─── Constants ──────────────────────────────────────────────
|
|
21
|
+
const PLUGIN_NAME = "@rachel_rotenberg/ai-contribution-tracker";
|
|
22
|
+
const HOOK_MARKER = "AI_IMPACT_PENDING";
|
|
23
|
+
|
|
24
|
+
// ─── Logging helpers ────────────────────────────────────────
|
|
25
|
+
function ok(msg) { console.log(` \u2713 ${msg}`); }
|
|
26
|
+
function skip(msg) { console.log(` - ${msg}`); }
|
|
27
|
+
function warn(msg) { console.log(` ! ${msg}`); }
|
|
28
|
+
function fail(msg) { console.error(` \u2717 ${msg}`); }
|
|
29
|
+
|
|
30
|
+
// ─── Git hook body (shell script, runs on all platforms via Git Bash) ───
|
|
31
|
+
const HOOK_BODY = [
|
|
32
|
+
"",
|
|
33
|
+
"# AI Contribution Tracker \u2014 reads AI_IMPACT_PENDING flag",
|
|
34
|
+
'IMPACT_FLAG=$(git rev-parse --git-path AI_IMPACT_PENDING)',
|
|
35
|
+
'STATE_FILE=$(git rev-parse --git-path ai-tracker-state.json)',
|
|
36
|
+
'if [ -f "$IMPACT_FLAG" ]; then',
|
|
37
|
+
' MARKER=$(cat "$IMPACT_FLAG")',
|
|
38
|
+
' if [ -z "$MARKER" ]; then MARKER="Impacted by AI"; fi',
|
|
39
|
+
' if ! grep -qF "$MARKER" "$1"; then',
|
|
40
|
+
' echo "" >> "$1"',
|
|
41
|
+
' echo "$MARKER" >> "$1"',
|
|
42
|
+
' fi',
|
|
43
|
+
' rm "$IMPACT_FLAG"',
|
|
44
|
+
'fi',
|
|
45
|
+
'if [ -f "$STATE_FILE" ]; then rm "$STATE_FILE"; fi',
|
|
46
|
+
].join("\n");
|
|
47
|
+
|
|
48
|
+
// ─── Git hook installation ──────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** Append our hook snippet to an existing commit-msg hook or create a new one. */
|
|
51
|
+
function appendOrCreateHook(hooksDir) {
|
|
52
|
+
const hookPath = path.join(hooksDir, "commit-msg");
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(hookPath)) {
|
|
55
|
+
const existing = fs.readFileSync(hookPath, "utf8");
|
|
56
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
57
|
+
skip(`commit-msg hook already has AI tracker snippet: ${hookPath}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
fs.appendFileSync(hookPath, "\n" + HOOK_BODY + "\n");
|
|
61
|
+
ok(`Appended AI tracker snippet to existing hook: ${hookPath}`);
|
|
62
|
+
} else {
|
|
63
|
+
const content = ("#!/bin/sh\n" + HOOK_BODY + "\n").replace(/\r\n/g, "\n");
|
|
64
|
+
fs.writeFileSync(hookPath, content);
|
|
65
|
+
ok(`Created commit-msg hook: ${hookPath}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Make executable (no-op on Windows; Git for Windows handles this via Git Bash)
|
|
69
|
+
try { fs.chmodSync(hookPath, "755"); } catch { /* Windows */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Install the global git commit-msg hook. */
|
|
73
|
+
function installGitHook() {
|
|
74
|
+
console.log("\nGit commit-msg hook:");
|
|
75
|
+
|
|
76
|
+
// Check if core.hooksPath is already set
|
|
77
|
+
let existingPath = "";
|
|
78
|
+
try {
|
|
79
|
+
existingPath = execSync("git config --global core.hooksPath", {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
82
|
+
}).trim();
|
|
83
|
+
} catch {
|
|
84
|
+
// Not set — we'll create our own
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existingPath) {
|
|
88
|
+
// Use existing hooksPath directory
|
|
89
|
+
fs.mkdirSync(existingPath, { recursive: true });
|
|
90
|
+
appendOrCreateHook(existingPath);
|
|
91
|
+
} else {
|
|
92
|
+
const hooksDir = path.join(os.homedir(), ".config", "ai-contribution-tracker", "git-hooks");
|
|
93
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
94
|
+
appendOrCreateHook(hooksDir);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const gitPath = hooksDir.replace(/\\/g, "/");
|
|
98
|
+
execFileSync("git", ["config", "--global", "core.hooksPath", gitPath], {
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
101
|
+
});
|
|
102
|
+
ok(`Set git global core.hooksPath: ${gitPath}`);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
fail(`Could not set core.hooksPath: ${e.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── OpenCode plugin registration ───────────────────────────
|
|
110
|
+
|
|
111
|
+
/** Add the plugin to the "plugin" array in an opencode.json file. */
|
|
112
|
+
function addPluginToConfig(configDir) {
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
115
|
+
const configPath = path.join(configDir, "opencode.json");
|
|
116
|
+
|
|
117
|
+
let config = {};
|
|
118
|
+
if (fs.existsSync(configPath)) {
|
|
119
|
+
try {
|
|
120
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
121
|
+
} catch {
|
|
122
|
+
// Malformed JSON — back up and start fresh
|
|
123
|
+
fs.copyFileSync(configPath, configPath + ".bak");
|
|
124
|
+
warn(`Backed up malformed ${configPath} to ${configPath}.bak`);
|
|
125
|
+
config = {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let plugins = config.plugin;
|
|
130
|
+
if (!Array.isArray(plugins)) {
|
|
131
|
+
plugins = [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if already registered (plain string or [name, options] tuple)
|
|
135
|
+
const alreadyRegistered = plugins.some(
|
|
136
|
+
(p) => (typeof p === "string" ? p : p[0]) === PLUGIN_NAME
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (alreadyRegistered) {
|
|
140
|
+
skip(`Plugin already in: ${configPath}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
plugins.push(PLUGIN_NAME);
|
|
145
|
+
config.plugin = plugins;
|
|
146
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
147
|
+
ok(`Added plugin to: ${configPath}`);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
fail(`Could not update ${configDir}: ${e.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function installOpenCodePlugin() {
|
|
154
|
+
console.log("\nOpenCode plugin:");
|
|
155
|
+
const configDir = path.join(os.homedir(), ".config", "opencode");
|
|
156
|
+
addPluginToConfig(configDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Status check ───────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function showStatus() {
|
|
162
|
+
console.log("\nAI Contribution Tracker \u2014 Status\n");
|
|
163
|
+
|
|
164
|
+
// Git hook
|
|
165
|
+
let hooksPath = "";
|
|
166
|
+
try {
|
|
167
|
+
hooksPath = execSync("git config --global core.hooksPath", {
|
|
168
|
+
encoding: "utf8",
|
|
169
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
170
|
+
}).trim();
|
|
171
|
+
} catch { /* not set */ }
|
|
172
|
+
|
|
173
|
+
if (hooksPath) {
|
|
174
|
+
const hookFile = path.join(hooksPath, "commit-msg");
|
|
175
|
+
if (fs.existsSync(hookFile) && fs.readFileSync(hookFile, "utf8").includes(HOOK_MARKER)) {
|
|
176
|
+
ok(`Git hook installed: ${hookFile}`);
|
|
177
|
+
} else {
|
|
178
|
+
warn(`core.hooksPath set to ${hooksPath} but no AI tracker snippet found`);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
warn("No global core.hooksPath set \u2014 git hook not installed");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// OpenCode plugin
|
|
185
|
+
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
186
|
+
if (fs.existsSync(configPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
189
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
190
|
+
const found = plugins.some(
|
|
191
|
+
(p) => (typeof p === "string" ? p : p[0]) === PLUGIN_NAME
|
|
192
|
+
);
|
|
193
|
+
if (found) {
|
|
194
|
+
ok(`OpenCode plugin registered: ${configPath}`);
|
|
195
|
+
} else {
|
|
196
|
+
warn(`opencode.json exists but plugin not in plugin array: ${configPath}`);
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
warn(`Could not parse: ${configPath}`);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
skip(`No opencode.json found at: ${configPath}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Uninstall ──────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function removeGitHook() {
|
|
209
|
+
console.log("\nGit commit-msg hook:");
|
|
210
|
+
|
|
211
|
+
let hooksPath = "";
|
|
212
|
+
try {
|
|
213
|
+
hooksPath = execSync("git config --global core.hooksPath", {
|
|
214
|
+
encoding: "utf8",
|
|
215
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
216
|
+
}).trim();
|
|
217
|
+
} catch { /* not set */ }
|
|
218
|
+
|
|
219
|
+
if (!hooksPath) {
|
|
220
|
+
skip("No global core.hooksPath set \u2014 nothing to remove");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hookFile = path.join(hooksPath, "commit-msg");
|
|
225
|
+
if (!fs.existsSync(hookFile)) {
|
|
226
|
+
skip(`No commit-msg hook at: ${hookFile}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const content = fs.readFileSync(hookFile, "utf8");
|
|
231
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
232
|
+
skip("commit-msg hook does not contain AI tracker snippet");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const lines = content.split("\n");
|
|
237
|
+
const filtered = [];
|
|
238
|
+
let skipping = false;
|
|
239
|
+
|
|
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]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const remaining = filtered.join("\n").trim();
|
|
261
|
+
if (remaining === "#!/bin/sh" || remaining === "") {
|
|
262
|
+
fs.unlinkSync(hookFile);
|
|
263
|
+
ok(`Removed commit-msg hook: ${hookFile}`);
|
|
264
|
+
|
|
265
|
+
const ourDefaultDir = path.join(os.homedir(), ".config", "ai-contribution-tracker", "git-hooks");
|
|
266
|
+
const normalizedHooksPath = hooksPath.replace(/\\/g, "/");
|
|
267
|
+
const normalizedOurDir = ourDefaultDir.replace(/\\/g, "/");
|
|
268
|
+
if (normalizedHooksPath === normalizedOurDir) {
|
|
269
|
+
try {
|
|
270
|
+
execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"], {
|
|
271
|
+
encoding: "utf8",
|
|
272
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
273
|
+
});
|
|
274
|
+
ok("Unset git global core.hooksPath");
|
|
275
|
+
} catch { }
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
fs.writeFileSync(hookFile, remaining + "\n");
|
|
279
|
+
ok(`Removed AI tracker snippet from: ${hookFile}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function removeOpenCodePlugin() {
|
|
284
|
+
console.log("\nOpenCode plugin:");
|
|
285
|
+
|
|
286
|
+
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
287
|
+
if (!fs.existsSync(configPath)) {
|
|
288
|
+
skip("No opencode.json found");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
294
|
+
if (!Array.isArray(config.plugin)) {
|
|
295
|
+
skip("No plugin array in opencode.json");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const before = config.plugin.length;
|
|
300
|
+
config.plugin = config.plugin.filter(
|
|
301
|
+
(p) => (typeof p === "string" ? p : p[0]) !== PLUGIN_NAME
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (config.plugin.length === before) {
|
|
305
|
+
skip("Plugin was not registered");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
310
|
+
ok(`Removed plugin from: ${configPath}`);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
fail(`Could not update ${configPath}: ${e.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── Main ───────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function printUsage() {
|
|
319
|
+
console.log(`
|
|
320
|
+
AI Contribution Tracker \u2014 CLI Installer
|
|
321
|
+
|
|
322
|
+
Installs git hooks and OpenCode plugin without requiring VS Code.
|
|
323
|
+
Works on macOS, Linux, and Windows.
|
|
324
|
+
|
|
325
|
+
Usage:
|
|
326
|
+
npx ${PLUGIN_NAME} init Install git hook + register OpenCode plugin
|
|
327
|
+
npx ${PLUGIN_NAME} status Show installation status
|
|
328
|
+
npx ${PLUGIN_NAME} remove Uninstall git hook + unregister plugin
|
|
329
|
+
npx ${PLUGIN_NAME} --help Show this help message
|
|
330
|
+
|
|
331
|
+
What gets installed:
|
|
332
|
+
1. Global git commit-msg hook (tags every AI-assisted commit)
|
|
333
|
+
2. OpenCode plugin entry in ~/.config/opencode/opencode.json
|
|
334
|
+
`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const command = process.argv[2];
|
|
338
|
+
|
|
339
|
+
switch (command) {
|
|
340
|
+
case "init":
|
|
341
|
+
case "install":
|
|
342
|
+
console.log("\nAI Contribution Tracker \u2014 Installing...\n");
|
|
343
|
+
console.log(`Platform: ${process.platform} (${os.arch()})`);
|
|
344
|
+
installGitHook();
|
|
345
|
+
installOpenCodePlugin();
|
|
346
|
+
console.log("\nDone. All future AI-assisted commits will be tagged automatically.\n");
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case "status":
|
|
350
|
+
showStatus();
|
|
351
|
+
break;
|
|
352
|
+
|
|
353
|
+
case "remove":
|
|
354
|
+
case "uninstall":
|
|
355
|
+
console.log("\nAI Contribution Tracker \u2014 Removing...");
|
|
356
|
+
removeGitHook();
|
|
357
|
+
removeOpenCodePlugin();
|
|
358
|
+
console.log("\nDone.\n");
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case "--help":
|
|
362
|
+
case "-h":
|
|
363
|
+
case "help":
|
|
364
|
+
printUsage();
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
default:
|
|
368
|
+
if (command) {
|
|
369
|
+
console.error(`Unknown command: ${command}\n`);
|
|
370
|
+
}
|
|
371
|
+
printUsage();
|
|
372
|
+
process.exit(command ? 1 : 0);
|
|
373
|
+
}
|
|
374
|
+
|
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) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rachel_rotenberg/ai-contribution-tracker",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "OpenCode plugin
|
|
3
|
+
"version": "1.0.20",
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ai-contribution-tracker": "cli.js"
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
8
|
-
"index.ts"
|
|
11
|
+
"index.ts",
|
|
12
|
+
"cli.js"
|
|
9
13
|
],
|
|
10
14
|
"exports": {
|
|
11
15
|
".": {
|
|
@@ -24,9 +28,9 @@
|
|
|
24
28
|
"license": "MIT",
|
|
25
29
|
"repository": {
|
|
26
30
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/YoavLax/AI-Contribution-Tracker"
|
|
31
|
+
"url": "git+https://github.com/YoavLax/AI-Contribution-Tracker.git"
|
|
28
32
|
},
|
|
29
33
|
"peerDependencies": {
|
|
30
34
|
"@opencode-ai/plugin": ">=1.0.0"
|
|
31
35
|
}
|
|
32
|
-
}
|
|
36
|
+
}
|