claude-attribution 1.2.5 → 1.2.8

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.
@@ -5,21 +5,12 @@
5
5
  *
6
6
  * If no path is given, installs into the current working directory.
7
7
  */
8
- import {
9
- readFile,
10
- writeFile,
11
- appendFile,
12
- mkdir,
13
- mkdtemp,
14
- unlink,
15
- rmdir,
16
- } from "fs/promises";
8
+ import { readFile, writeFile, appendFile, mkdir } from "fs/promises";
17
9
  import { existsSync } from "fs";
18
10
  import { execFile } from "child_process";
19
11
  import { promisify } from "util";
20
- import { resolve, join } from "path";
21
- import { tmpdir } from "os";
22
12
  import { createInterface } from "readline";
13
+ import { resolve, join } from "path";
23
14
  import {
24
15
  ATTRIBUTION_ROOT,
25
16
  mergeHooks,
@@ -29,209 +20,122 @@ import {
29
20
  detectHookManager,
30
21
  type HooksConfig,
31
22
  } from "./shared.ts";
23
+ import { configureRequiredCheck } from "./branch-protection.ts";
24
+ import {
25
+ listMinimapNotes,
26
+ writeMinimap,
27
+ buildAllAiMinimap,
28
+ buildAiSinceMinimap,
29
+ } from "../attribution/minimap.ts";
32
30
 
33
31
  const execFileAsync = promisify(execFile);
34
32
 
35
33
  const CLI_BIN = resolve(ATTRIBUTION_ROOT, "bin", "claude-attribution");
36
34
 
37
- /**
38
- * The exact GitHub Actions job name written into our workflow template.
39
- * Must match the `name:` field of the `metrics` job in pr-metrics-workflow.yml.
40
- */
41
- const WORKFLOW_CHECK_NAME = "Claude Code Attribution Metrics";
35
+ // ─── Baseline init prompt ────────────────────────────────────────────────────
42
36
 
43
- /** Extract "owner/repo" from an origin remote URL (SSH or HTTPS). */
44
- function remoteUrlToSlug(url: string): string | null {
45
- const m =
46
- url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/) ??
47
- url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
48
- return m?.[1] ?? null;
49
- }
37
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
50
38
 
51
- /** Call `gh api <path>` and return parsed JSON, or null on any error. */
52
- async function ghApiGet(path: string): Promise<unknown> {
53
- try {
54
- const { stdout } = (await execFileAsync("gh", [
55
- "api",
56
- path,
57
- ])) as unknown as {
58
- stdout: string;
59
- };
60
- return JSON.parse(stdout) as unknown;
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- /** Prompt the user for a yes/no answer. Returns false in non-TTY contexts. */
67
- async function promptYesNo(question: string): Promise<boolean> {
68
- if (!process.stdin.isTTY) return false;
39
+ async function promptLine(question: string): Promise<string> {
69
40
  const rl = createInterface({ input: process.stdin, output: process.stdout });
70
41
  return new Promise((resolve) => {
71
42
  rl.question(question, (answer) => {
72
43
  rl.close();
73
- resolve(answer.trim().toLowerCase() === "y");
44
+ resolve(answer.trim());
74
45
  });
75
46
  });
76
47
  }
77
48
 
78
- function printRequiredCheckNote(branch: string): void {
79
- console.log(
80
- `\n ℹ️ To block merges when this workflow fails, add '${WORKFLOW_CHECK_NAME}'`,
81
- );
82
- console.log(
83
- ` to required status checks for '${branch}' in Settings → Branches.`,
84
- );
85
- }
86
-
87
49
  /**
88
- * PATCH the required status checks for a branch via the GitHub API.
89
- * Writes the JSON body to a temp file to avoid arg-length issues.
50
+ * After install, ask the developer how to treat existing code in the minimap.
51
+ * Skipped if the minimap was already initialized (flag file or existing notes).
52
+ * Never throws — any failure falls back to a printed note.
90
53
  */
91
- async function patchRequiredChecks(
92
- slug: string,
93
- branch: string,
94
- body: unknown,
54
+ async function promptBaselineInit(
55
+ repoRoot: string,
56
+ claudeDir: string,
95
57
  ): Promise<void> {
96
- const tmpDir = await mkdtemp(join(tmpdir(), "claude-attribution-api-"));
97
- const tmpFile = join(tmpDir, "body.json");
98
- try {
99
- await writeFile(tmpFile, JSON.stringify(body), { flag: "wx" });
100
- await execFileAsync("gh", [
101
- "api",
102
- `repos/${slug}/branches/${branch}/protection/required_status_checks`,
103
- "--method",
104
- "PATCH",
105
- "--input",
106
- tmpFile,
107
- ]);
108
- } finally {
109
- await unlink(tmpFile).catch(() => {});
110
- await rmdir(tmpDir).catch(() => {});
111
- }
112
- }
58
+ const flagPath = join(claudeDir, "attribution-state", "baseline-initialized");
113
59
 
114
- /**
115
- * After installing the workflow, check branch protection / rulesets and offer
116
- * to add our workflow job as a required status check.
117
- *
118
- * - Classic branch protection: fully automatic (detect → prompt → PATCH)
119
- * - Rulesets: detect only → informational note (ruleset API requires full PUT)
120
- * - Any error or non-TTY: fall back to informational note
121
- */
122
- async function maybeAddRequiredCheck(repoRoot: string): Promise<void> {
123
60
  try {
124
- // Resolve the GitHub slug from the origin remote
125
- const { stdout: remoteOut } = (await execFileAsync(
126
- "git",
127
- ["remote", "get-url", "origin"],
128
- { cwd: repoRoot },
129
- )) as unknown as { stdout: string };
130
- const slug = remoteUrlToSlug(remoteOut.trim());
131
- if (!slug) return;
132
-
133
- // Get the default branch name
134
- const repoData = await ghApiGet(`repos/${slug}`);
135
- const branch = (repoData as { default_branch?: string } | null)
136
- ?.default_branch;
137
- if (!branch) return;
138
-
139
- // --- Classic branch protection ---
140
- const protection = await ghApiGet(
141
- `repos/${slug}/branches/${branch}/protection`,
142
- );
143
-
144
- if (!protection) {
145
- // No classic protection — check rulesets (detect-only)
146
- const rulesets = await ghApiGet(`repos/${slug}/rulesets`);
147
- if (Array.isArray(rulesets) && rulesets.length > 0) {
148
- // Check if any ruleset already requires our check
149
- const alreadyRequired = (
150
- rulesets as Array<{
151
- rules?: Array<{
152
- type: string;
153
- parameters?: {
154
- required_status_checks?: Array<{ context: string }>;
155
- };
156
- }>;
157
- }>
158
- ).some((rs) =>
159
- rs.rules?.some(
160
- (r) =>
161
- r.type === "required_status_checks" &&
162
- r.parameters?.required_status_checks?.some(
163
- (c) => c.context === WORKFLOW_CHECK_NAME,
164
- ),
165
- ),
166
- );
167
- if (alreadyRequired) {
168
- console.log(
169
- `✓ '${WORKFLOW_CHECK_NAME}' already a required status check`,
170
- );
171
- return;
172
- }
173
- console.log(
174
- `\n ℹ️ Ruleset branch protection detected on '${branch}'.`,
175
- );
176
- console.log(
177
- ` Add '${WORKFLOW_CHECK_NAME}' to required status checks`,
178
- );
179
- console.log(
180
- ` in Settings → Rules to block merges on workflow failure.`,
181
- );
182
- }
61
+ // Idempotent: skip if already set
62
+ if (existsSync(flagPath)) return;
63
+ const existingNotes = await listMinimapNotes(repoRoot);
64
+ if (existingNotes.length > 0) return;
65
+
66
+ // Verify there are commits to work with
67
+ try {
68
+ await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
69
+ } catch {
70
+ console.log(
71
+ "\n ℹ️ Run 'claude-attribution init --ai' after your first commit to set the attribution baseline.",
72
+ );
183
73
  return;
184
74
  }
185
75
 
186
- // Extract current required check names from classic protection
187
- type Check = { context: string; app_id: number };
188
- const prot = protection as {
189
- required_status_checks?: {
190
- strict: boolean;
191
- contexts?: string[];
192
- checks?: Check[];
193
- };
194
- };
195
- const existingChecks: Check[] =
196
- prot.required_status_checks?.checks ??
197
- (prot.required_status_checks?.contexts ?? []).map((c) => ({
198
- context: c,
199
- app_id: -1,
200
- }));
201
- const strict = prot.required_status_checks?.strict ?? false;
202
-
203
- if (existingChecks.some((c) => c.context === WORKFLOW_CHECK_NAME)) {
204
- console.log(`✓ '${WORKFLOW_CHECK_NAME}' already a required status check`);
76
+ if (!process.stdin.isTTY) {
77
+ console.log(
78
+ "\n ℹ️ Run 'claude-attribution init [--ai | --ai-since <YYYY-MM-DD>]' to set the attribution baseline.",
79
+ );
205
80
  return;
206
81
  }
207
82
 
208
- // Prompt
209
- console.log(`\n Branch protection is active on '${branch}'.`);
210
- const yes = await promptYesNo(
211
- ` Add '${WORKFLOW_CHECK_NAME}' as a required status check? [y/N] `,
83
+ console.log(
84
+ "\n Attribution baseline how should existing code be treated?",
212
85
  );
213
- if (!yes) {
214
- printRequiredCheckNote(branch);
215
- return;
216
- }
217
-
218
- await patchRequiredChecks(slug, branch, {
219
- strict,
220
- checks: [...existingChecks, { context: WORKFLOW_CHECK_NAME, app_id: -1 }],
221
- });
222
86
  console.log(
223
- `✓ Added '${WORKFLOW_CHECK_NAME}' as a required status check on '${branch}'`,
87
+ " [1] AI — all current code was written with Claude Code",
224
88
  );
225
- } catch {
226
- // Any failure — don't break install, just print the note
227
89
  console.log(
228
- `\n ℹ️ Could not configure required status checks automatically.`,
90
+ " [2] Human — assume human-written (AI accumulates from here)",
229
91
  );
230
92
  console.log(
231
- ` To block merges on workflow failure, add '${WORKFLOW_CHECK_NAME}'`,
93
+ " [3] AI since — mark commits after a date as AI-written",
232
94
  );
233
95
  console.log(
234
- ` to required status checks in your branch protection settings.`,
96
+ " [4] Skip — decide later (run: claude-attribution init --ai)",
97
+ );
98
+ const raw = await promptLine(" Choice [2]: ");
99
+ const choice = raw === "" ? 2 : parseInt(raw, 10);
100
+
101
+ if (choice === 1) {
102
+ console.log(" Marking all files as AI-written...");
103
+ const result = await buildAllAiMinimap(repoRoot);
104
+ await writeMinimap(result, repoRoot);
105
+ await writeFile(flagPath, "ai");
106
+ const { pctAi, total } = result.totals;
107
+ console.log(
108
+ `✓ Baseline set: AI (${result.files.length} files, ${total} lines, ${pctAi}% AI)`,
109
+ );
110
+ } else if (choice === 2) {
111
+ await writeFile(flagPath, "human");
112
+ console.log(
113
+ "✓ Baseline set: human (AI attribution accumulates from this point)",
114
+ );
115
+ } else if (choice === 3) {
116
+ // Prompt for date with validation loop
117
+ let date = "";
118
+ while (true) {
119
+ date = await promptLine(" Start date (YYYY-MM-DD): ");
120
+ if (DATE_RE.test(date)) break;
121
+ console.log(
122
+ ` Invalid format: ${date || "(empty)"}. Use YYYY-MM-DD (e.g. 2026-01-15).`,
123
+ );
124
+ }
125
+ console.log(` Marking files changed since ${date} as AI-written...`);
126
+ const result = await buildAiSinceMinimap(repoRoot, date);
127
+ await writeMinimap(result, repoRoot);
128
+ await writeFile(flagPath, `ai-since:${date}`);
129
+ const { pctAi, total } = result.totals;
130
+ console.log(
131
+ `✓ Baseline set: AI since ${date} (${result.files.length} files, ${total} lines, ${pctAi}% AI)`,
132
+ );
133
+ }
134
+ // choice === 4 or unrecognized: Skip — write no flag, ask again next install
135
+ } catch {
136
+ console.log("\n ℹ️ Could not set attribution baseline automatically.");
137
+ console.log(
138
+ " Run 'claude-attribution init [--ai | --ai-since <YYYY-MM-DD>]' manually.",
235
139
  );
236
140
  }
237
141
  }
@@ -387,9 +291,12 @@ async function main() {
387
291
  );
388
292
 
389
293
  // 5. Check branch protection and offer to add required status check
390
- await maybeAddRequiredCheck(targetRepo);
294
+ await configureRequiredCheck(targetRepo);
295
+
296
+ // 6. Prompt for attribution baseline (skipped on re-runs)
297
+ await promptBaselineInit(targetRepo, claudeDir);
391
298
 
392
- // 6. Record installed version for auto-upgrade tracking
299
+ // 7. Record installed version for auto-upgrade tracking
393
300
  const pkg = JSON.parse(
394
301
  await readFile(join(ATTRIBUTION_ROOT, "package.json"), "utf8"),
395
302
  ) as { version: string };
@@ -23,7 +23,10 @@ jobs:
23
23
  git fetch origin refs/notes/claude-attribution-map:refs/notes/claude-attribution-map || true
24
24
 
25
25
  - name: Install claude-attribution
26
- run: npm install -g claude-attribution
26
+ run: |
27
+ npm config set prefix "${HOME}/.npm-global"
28
+ npm install -g claude-attribution
29
+ echo "${HOME}/.npm-global/bin" >> "$GITHUB_PATH"
27
30
 
28
31
  - name: Generate metrics
29
32
  run: claude-attribution metrics > /tmp/claude-attribution-metrics.md || true