gitfamiliar 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GitFamiliar Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # GitFamiliar
2
+
3
+ **Visualize your code familiarity from Git history.**
4
+
5
+ Existing tools (git-fame, GitHub Contributors, etc.) measure _how much_ you've written. GitFamiliar estimates _how well_ you understand the codebase. Built for engineers joining a new project, it lets you and your team objectively track onboarding progress.
6
+
7
+ ```
8
+ GitFamiliar — my-project (Binary mode)
9
+
10
+ Overall: 58/172 files (34%)
11
+
12
+ src/
13
+ auth/ ████████░░ 80% (4/5 files)
14
+ api/ ███░░░░░░░ 30% (6/20 files)
15
+ components/ █░░░░░░░░░ 12% (3/25 files)
16
+ utils/ ██████████ 100% (8/8 files)
17
+ tests/ ░░░░░░░░░░ 0% (0/14 files)
18
+ config/ ██████░░░░ 60% (3/5 files)
19
+
20
+ Written: 42 files | Reviewed: 23 files | Both: 7 files
21
+ ```
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npx gitfamiliar
27
+ ```
28
+
29
+ Or install globally:
30
+
31
+ ```bash
32
+ npm install -g gitfamiliar
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Run inside any Git repository:
38
+
39
+ ```bash
40
+ # Binary mode (default) — which files have you touched?
41
+ gitfamiliar
42
+
43
+ # Authorship mode — how much of the current code did you write? (git blame)
44
+ gitfamiliar --mode authorship
45
+
46
+ # Review Coverage — which files have you reviewed via PR?
47
+ gitfamiliar --mode review-coverage
48
+
49
+ # Weighted mode — combined score from blame, commits, and reviews
50
+ gitfamiliar --mode weighted
51
+
52
+ # HTML treemap report (opens in browser)
53
+ gitfamiliar --html
54
+
55
+ # Check a specific user
56
+ gitfamiliar --user kota
57
+
58
+ # Filter by how you touched the code
59
+ gitfamiliar --filter written # only files you committed to
60
+ gitfamiliar --filter reviewed # only files you reviewed
61
+
62
+ # Expiration policies
63
+ gitfamiliar --expiration time:180d # expire after 180 days
64
+ gitfamiliar --expiration change:50% # expire if 50%+ changed
65
+ gitfamiliar --expiration combined:365d:50%
66
+
67
+ # Custom weights for weighted mode
68
+ gitfamiliar --mode weighted --weights "0.5,0.35,0.15"
69
+ ```
70
+
71
+ ## Scoring Modes
72
+
73
+ ### Binary (default)
74
+
75
+ Files are either "read" or "unread". A file counts as read if you've committed to it or approved a PR containing it.
76
+
77
+ ```
78
+ score = read files / total files
79
+ ```
80
+
81
+ Best for: **New team members** tracking onboarding progress.
82
+
83
+ ### Authorship
84
+
85
+ Your share of the current codebase, based on `git blame`.
86
+
87
+ ```
88
+ score = your blame lines / total lines
89
+ ```
90
+
91
+ Best for: **Tech leads** assessing bus factor and code ownership distribution.
92
+
93
+ ### Review Coverage
94
+
95
+ Files you've reviewed through PR approvals or comments (excluding your own commits).
96
+
97
+ ```
98
+ score = reviewed files / total files
99
+ ```
100
+
101
+ Best for: **Senior engineers** tracking review breadth. Requires a GitHub token.
102
+
103
+ ### Weighted
104
+
105
+ Combines blame, commit frequency, and review signals with configurable weights and time decay.
106
+
107
+ ```
108
+ score = 0.5 × blame_score + 0.35 × commit_score + 0.15 × review_score
109
+ ```
110
+
111
+ Commit contributions use sigmoid normalization and exponential recency decay (half-life: 180 days). Review scores account for PR size (attention dilution).
112
+
113
+ Best for: **Power users** who want the most nuanced picture.
114
+
115
+ ## HTML Report
116
+
117
+ Use `--html` to generate an interactive treemap visualization:
118
+
119
+ - **Area** = lines of code (file/folder volume)
120
+ - **Color** = familiarity score (red → yellow → green)
121
+ - Click folders to drill down
122
+ - Toggle between All / Written / Reviewed views
123
+ - Hover for detailed scores
124
+
125
+ ## File Filtering
126
+
127
+ GitFamiliar ignores lock files, build outputs, and generated code by default. Customize by creating `.gitfamiliarignore` in your repo root (same syntax as `.gitignore`):
128
+
129
+ ```gitignore
130
+ # Example: also ignore vendor code
131
+ vendor/
132
+ third_party/
133
+ ```
134
+
135
+ Default exclusions: `package-lock.json`, `yarn.lock`, `*.min.js`, `*.map`, `dist/`, `build/`, etc.
136
+
137
+ ## Expiration Policies
138
+
139
+ Control whether "read" status expires over time:
140
+
141
+ | Policy | Flag | Behavior |
142
+ |---|---|---|
143
+ | Never (default) | `--expiration never` | Once read, always read |
144
+ | Time-based | `--expiration time:180d` | Expires 180 days after last touch |
145
+ | Change-based | `--expiration change:50%` | Expires if 50%+ of the file changed since your last touch |
146
+ | Combined | `--expiration combined:365d:50%` | Expires if either condition is met |
147
+
148
+ ## GitHub Integration
149
+
150
+ For review-related features (Review Coverage mode, reviewed files in Binary mode), set a GitHub token:
151
+
152
+ ```bash
153
+ # Option 1: environment variable
154
+ export GITHUB_TOKEN=ghp_xxx
155
+
156
+ # Option 2: GitHub CLI (auto-detected)
157
+ gh auth login
158
+ ```
159
+
160
+ ## Requirements
161
+
162
+ - Node.js >= 18
163
+ - Git
164
+ - GitHub token (optional, for review features)
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ computeFamiliarity,
4
+ parseExpirationConfig
5
+ } from "../chunk-DW2PHZVZ.js";
6
+
7
+ // src/cli/index.ts
8
+ import { Command } from "commander";
9
+
10
+ // src/core/types.ts
11
+ var DEFAULT_WEIGHTS = {
12
+ blame: 0.5,
13
+ commit: 0.35,
14
+ review: 0.15
15
+ };
16
+ var DEFAULT_EXPIRATION = {
17
+ policy: "never"
18
+ };
19
+
20
+ // src/cli/options.ts
21
+ function parseOptions(raw, repoPath) {
22
+ const mode = validateMode(raw.mode || "binary");
23
+ const filter = validateFilter(raw.filter || "all");
24
+ let weights = DEFAULT_WEIGHTS;
25
+ if (raw.weights) {
26
+ weights = parseWeights(raw.weights);
27
+ }
28
+ const expiration = raw.expiration ? parseExpirationConfig(raw.expiration) : DEFAULT_EXPIRATION;
29
+ return {
30
+ mode,
31
+ user: raw.user,
32
+ filter,
33
+ expiration,
34
+ html: raw.html || false,
35
+ weights,
36
+ repoPath
37
+ };
38
+ }
39
+ function validateMode(mode) {
40
+ const valid = ["binary", "authorship", "review-coverage", "weighted"];
41
+ if (!valid.includes(mode)) {
42
+ throw new Error(`Invalid mode: "${mode}". Valid modes: ${valid.join(", ")}`);
43
+ }
44
+ return mode;
45
+ }
46
+ function validateFilter(filter) {
47
+ const valid = ["all", "written", "reviewed"];
48
+ if (!valid.includes(filter)) {
49
+ throw new Error(`Invalid filter: "${filter}". Valid filters: ${valid.join(", ")}`);
50
+ }
51
+ return filter;
52
+ }
53
+ function parseWeights(s) {
54
+ const parts = s.split(",").map(Number);
55
+ if (parts.length !== 3 || parts.some(isNaN)) {
56
+ throw new Error(`Invalid weights: "${s}". Expected format: "0.5,0.35,0.15"`);
57
+ }
58
+ const sum = parts[0] + parts[1] + parts[2];
59
+ if (Math.abs(sum - 1) > 0.01) {
60
+ throw new Error(`Weights must sum to 1.0, got ${sum}`);
61
+ }
62
+ return { blame: parts[0], commit: parts[1], review: parts[2] };
63
+ }
64
+
65
+ // src/cli/output/terminal.ts
66
+ import chalk from "chalk";
67
+ var BAR_WIDTH = 10;
68
+ var FILLED_CHAR = "\u2588";
69
+ var EMPTY_CHAR = "\u2591";
70
+ function makeBar(score) {
71
+ const filled = Math.round(score * BAR_WIDTH);
72
+ const empty = BAR_WIDTH - filled;
73
+ const bar = FILLED_CHAR.repeat(filled) + EMPTY_CHAR.repeat(empty);
74
+ if (score >= 0.8) return chalk.green(bar);
75
+ if (score >= 0.5) return chalk.yellow(bar);
76
+ if (score > 0) return chalk.red(bar);
77
+ return chalk.gray(bar);
78
+ }
79
+ function formatPercent(score) {
80
+ return `${Math.round(score * 100)}%`;
81
+ }
82
+ function getModeLabel(mode) {
83
+ switch (mode) {
84
+ case "binary":
85
+ return "Binary mode";
86
+ case "authorship":
87
+ return "Authorship mode";
88
+ case "review-coverage":
89
+ return "Review Coverage mode";
90
+ case "weighted":
91
+ return "Weighted mode";
92
+ default:
93
+ return mode;
94
+ }
95
+ }
96
+ function renderFolder(node, indent, mode, maxDepth) {
97
+ const lines = [];
98
+ const prefix = " ".repeat(indent);
99
+ const sorted = [...node.children].sort((a, b) => {
100
+ if (a.type !== b.type) return a.type === "folder" ? -1 : 1;
101
+ return a.path.localeCompare(b.path);
102
+ });
103
+ for (const child of sorted) {
104
+ if (child.type === "folder") {
105
+ const folder = child;
106
+ const name = folder.path.split("/").pop() + "/";
107
+ const bar = makeBar(folder.score);
108
+ const pct = formatPercent(folder.score);
109
+ if (mode === "binary") {
110
+ const readCount = folder.readCount || 0;
111
+ lines.push(
112
+ `${prefix}${chalk.bold(name.padEnd(16))} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`
113
+ );
114
+ } else {
115
+ lines.push(
116
+ `${prefix}${chalk.bold(name.padEnd(16))} ${bar} ${pct.padStart(4)}`
117
+ );
118
+ }
119
+ if (indent < maxDepth) {
120
+ lines.push(...renderFolder(folder, indent + 1, mode, maxDepth));
121
+ }
122
+ }
123
+ }
124
+ return lines;
125
+ }
126
+ function renderTerminal(result) {
127
+ const { tree, repoName, mode } = result;
128
+ console.log("");
129
+ console.log(chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`));
130
+ console.log("");
131
+ if (mode === "binary") {
132
+ const readCount = tree.readCount || 0;
133
+ const pct = formatPercent(tree.score);
134
+ console.log(`Overall: ${readCount}/${tree.fileCount} files (${pct})`);
135
+ } else {
136
+ const pct = formatPercent(tree.score);
137
+ console.log(`Overall: ${pct}`);
138
+ }
139
+ console.log("");
140
+ const folderLines = renderFolder(tree, 1, mode, 2);
141
+ for (const line of folderLines) {
142
+ console.log(line);
143
+ }
144
+ console.log("");
145
+ if (mode === "binary") {
146
+ const { writtenCount, reviewedCount, bothCount } = result;
147
+ console.log(
148
+ `Written: ${writtenCount} files | Reviewed: ${reviewedCount} files | Both: ${bothCount} files`
149
+ );
150
+ console.log("");
151
+ }
152
+ }
153
+
154
+ // src/cli/output/html.ts
155
+ import { writeFileSync } from "fs";
156
+ import { join } from "path";
157
+
158
+ // src/utils/open-browser.ts
159
+ async function openBrowser(filePath) {
160
+ try {
161
+ const open = await import("open");
162
+ await open.default(filePath);
163
+ } catch {
164
+ console.log(`Could not open browser automatically. Open this file manually:`);
165
+ console.log(` ${filePath}`);
166
+ }
167
+ }
168
+
169
+ // src/cli/output/html.ts
170
+ function generateTreemapHTML(result) {
171
+ const dataJson = JSON.stringify(result.tree);
172
+ const mode = result.mode;
173
+ const repoName = result.repoName;
174
+ return `<!DOCTYPE html>
175
+ <html lang="en">
176
+ <head>
177
+ <meta charset="UTF-8">
178
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
179
+ <title>GitFamiliar \u2014 ${repoName}</title>
180
+ <style>
181
+ * { margin: 0; padding: 0; box-sizing: border-box; }
182
+ body {
183
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
184
+ background: #1a1a2e;
185
+ color: #e0e0e0;
186
+ overflow: hidden;
187
+ }
188
+ #header {
189
+ padding: 16px 24px;
190
+ background: #16213e;
191
+ border-bottom: 1px solid #0f3460;
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: space-between;
195
+ }
196
+ #header h1 { font-size: 18px; color: #e94560; }
197
+ #header .info { font-size: 14px; color: #a0a0a0; }
198
+ #breadcrumb {
199
+ padding: 8px 24px;
200
+ background: #16213e;
201
+ font-size: 13px;
202
+ border-bottom: 1px solid #0f3460;
203
+ }
204
+ #breadcrumb span { cursor: pointer; color: #5eadf7; }
205
+ #breadcrumb span:hover { text-decoration: underline; }
206
+ #breadcrumb .sep { color: #666; margin: 0 4px; }
207
+ #controls {
208
+ padding: 8px 24px;
209
+ background: #16213e;
210
+ border-bottom: 1px solid #0f3460;
211
+ display: flex;
212
+ gap: 12px;
213
+ align-items: center;
214
+ }
215
+ #controls button {
216
+ padding: 4px 12px;
217
+ border: 1px solid #0f3460;
218
+ background: #1a1a2e;
219
+ color: #e0e0e0;
220
+ border-radius: 4px;
221
+ cursor: pointer;
222
+ font-size: 12px;
223
+ }
224
+ #controls button.active {
225
+ background: #e94560;
226
+ border-color: #e94560;
227
+ color: white;
228
+ }
229
+ #treemap { width: 100%; }
230
+ #tooltip {
231
+ position: absolute;
232
+ pointer-events: none;
233
+ background: rgba(22, 33, 62, 0.95);
234
+ border: 1px solid #0f3460;
235
+ border-radius: 6px;
236
+ padding: 10px 14px;
237
+ font-size: 13px;
238
+ line-height: 1.6;
239
+ display: none;
240
+ z-index: 100;
241
+ max-width: 300px;
242
+ }
243
+ #legend {
244
+ position: absolute;
245
+ bottom: 16px;
246
+ right: 16px;
247
+ background: rgba(22, 33, 62, 0.9);
248
+ border: 1px solid #0f3460;
249
+ border-radius: 6px;
250
+ padding: 10px;
251
+ font-size: 12px;
252
+ }
253
+ #legend .gradient-bar {
254
+ width: 120px;
255
+ height: 12px;
256
+ background: linear-gradient(to right, #e94560, #f5a623, #27ae60);
257
+ border-radius: 3px;
258
+ margin: 4px 0;
259
+ }
260
+ #legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }
261
+ </style>
262
+ </head>
263
+ <body>
264
+ <div id="header">
265
+ <h1>GitFamiliar \u2014 ${repoName}</h1>
266
+ <div class="info">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>
267
+ </div>
268
+ <div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
269
+ ${mode === "binary" ? `
270
+ <div id="controls">
271
+ <span style="font-size:12px;color:#888;">Filter:</span>
272
+ <button class="active" onclick="setFilter('all')">All</button>
273
+ <button onclick="setFilter('written')">Written only</button>
274
+ <button onclick="setFilter('reviewed')">Reviewed only</button>
275
+ </div>` : ""}
276
+ <div id="treemap"></div>
277
+ <div id="tooltip"></div>
278
+ <div id="legend">
279
+ <div>Familiarity</div>
280
+ <div class="gradient-bar"></div>
281
+ <div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
282
+ </div>
283
+
284
+ <script src="https://d3js.org/d3.v7.min.js"></script>
285
+ <script>
286
+ const rawData = ${dataJson};
287
+ const mode = "${mode}";
288
+ let currentFilter = 'all';
289
+ let currentPath = '';
290
+
291
+ function scoreColor(score) {
292
+ if (score <= 0) return '#e94560';
293
+ if (score >= 1) return '#27ae60';
294
+ if (score < 0.5) {
295
+ const t = score / 0.5;
296
+ return d3.interpolateRgb('#e94560', '#f5a623')(t);
297
+ }
298
+ const t = (score - 0.5) / 0.5;
299
+ return d3.interpolateRgb('#f5a623', '#27ae60')(t);
300
+ }
301
+
302
+ function getFileScore(file) {
303
+ if (mode !== 'binary') return file.score;
304
+ if (currentFilter === 'written') return file.isWritten ? 1 : 0;
305
+ if (currentFilter === 'reviewed') return file.isReviewed ? 1 : 0;
306
+ return file.score;
307
+ }
308
+
309
+ function findNode(node, path) {
310
+ if (node.path === path) return node;
311
+ if (node.children) {
312
+ for (const child of node.children) {
313
+ const found = findNode(child, path);
314
+ if (found) return found;
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+
320
+ function flattenFiles(node) {
321
+ const files = [];
322
+ function walk(n) {
323
+ if (n.type === 'file') {
324
+ files.push(n);
325
+ } else if (n.children) {
326
+ n.children.forEach(walk);
327
+ }
328
+ }
329
+ walk(node);
330
+ return files;
331
+ }
332
+
333
+ function render() {
334
+ const container = document.getElementById('treemap');
335
+ container.innerHTML = '';
336
+
337
+ const headerH = document.getElementById('header').offsetHeight;
338
+ const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;
339
+ const controlsEl = document.getElementById('controls');
340
+ const controlsH = controlsEl ? controlsEl.offsetHeight : 0;
341
+ const width = window.innerWidth;
342
+ const height = window.innerHeight - headerH - breadcrumbH - controlsH;
343
+
344
+ const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;
345
+ if (!targetNode) return;
346
+
347
+ const hierarchyData = {
348
+ name: targetNode.path || 'root',
349
+ children: (targetNode.children || []).map(function buildChild(c) {
350
+ if (c.type === 'file') {
351
+ return { name: c.path.split('/').pop(), data: c, value: Math.max(1, c.lines) };
352
+ }
353
+ return {
354
+ name: c.path.split('/').pop(),
355
+ data: c,
356
+ children: (c.children || []).map(buildChild),
357
+ };
358
+ }),
359
+ };
360
+
361
+ const root = d3.hierarchy(hierarchyData)
362
+ .sum(d => d.value || 0)
363
+ .sort((a, b) => (b.value || 0) - (a.value || 0));
364
+
365
+ d3.treemap()
366
+ .size([width, height])
367
+ .padding(2)
368
+ .paddingTop(18)
369
+ .round(true)(root);
370
+
371
+ const svg = d3.select('#treemap')
372
+ .append('svg')
373
+ .attr('width', width)
374
+ .attr('height', height);
375
+
376
+ const tooltip = document.getElementById('tooltip');
377
+
378
+ // Draw groups (folders)
379
+ const groups = svg.selectAll('g')
380
+ .data(root.descendants().filter(d => d.depth > 0))
381
+ .join('g')
382
+ .attr('transform', d => \`translate(\${d.x0},\${d.y0})\`);
383
+
384
+ groups.append('rect')
385
+ .attr('width', d => Math.max(0, d.x1 - d.x0))
386
+ .attr('height', d => Math.max(0, d.y1 - d.y0))
387
+ .attr('fill', d => {
388
+ if (d.data.data) {
389
+ const score = d.children ? d.data.data.score : getFileScore(d.data.data);
390
+ return scoreColor(score);
391
+ }
392
+ return '#333';
393
+ })
394
+ .attr('opacity', d => d.children ? 0.3 : 0.85)
395
+ .attr('stroke', '#1a1a2e')
396
+ .attr('stroke-width', 1)
397
+ .attr('rx', 2)
398
+ .style('cursor', d => d.children ? 'pointer' : 'default')
399
+ .on('click', (event, d) => {
400
+ if (d.children && d.data.data && d.data.data.type === 'folder') {
401
+ zoomTo(d.data.data.path);
402
+ }
403
+ })
404
+ .on('mouseover', (event, d) => {
405
+ if (!d.data.data) return;
406
+ const data = d.data.data;
407
+ const name = data.path || d.data.name;
408
+ const score = d.children ? data.score : getFileScore(data);
409
+ let html = \`<strong>\${name}</strong><br>Score: \${Math.round(score * 100)}%<br>Lines: \${data.lines}\`;
410
+ if (data.type === 'folder') {
411
+ html += \`<br>Files: \${data.fileCount}\`;
412
+ }
413
+ if (data.blameScore !== undefined) {
414
+ html += \`<br>Blame: \${Math.round(data.blameScore * 100)}%\`;
415
+ }
416
+ if (data.commitScore !== undefined) {
417
+ html += \`<br>Commit: \${Math.round(data.commitScore * 100)}%\`;
418
+ }
419
+ if (data.reviewScore !== undefined) {
420
+ html += \`<br>Review: \${Math.round(data.reviewScore * 100)}%\`;
421
+ }
422
+ tooltip.innerHTML = html;
423
+ tooltip.style.display = 'block';
424
+ })
425
+ .on('mousemove', (event) => {
426
+ tooltip.style.left = (event.pageX + 12) + 'px';
427
+ tooltip.style.top = (event.pageY - 12) + 'px';
428
+ })
429
+ .on('mouseout', () => {
430
+ tooltip.style.display = 'none';
431
+ });
432
+
433
+ // Labels
434
+ groups.append('text')
435
+ .attr('x', 4)
436
+ .attr('y', 13)
437
+ .attr('fill', '#fff')
438
+ .attr('font-size', '11px')
439
+ .attr('font-weight', d => d.children ? 'bold' : 'normal')
440
+ .text(d => {
441
+ const w = (d.x1 - d.x0);
442
+ const name = d.data.name || '';
443
+ if (w < 40) return '';
444
+ if (w < name.length * 7) return name.slice(0, Math.floor(w / 7)) + '..';
445
+ return name;
446
+ });
447
+ }
448
+
449
+ function zoomTo(path) {
450
+ currentPath = path;
451
+ updateBreadcrumb();
452
+ render();
453
+ }
454
+
455
+ function updateBreadcrumb() {
456
+ const el = document.getElementById('breadcrumb');
457
+ const parts = currentPath ? currentPath.split('/') : [];
458
+ let html = '<span onclick="zoomTo(\\'\\')">root</span>';
459
+ let accumulated = '';
460
+ for (const part of parts) {
461
+ accumulated = accumulated ? accumulated + '/' + part : part;
462
+ const p = accumulated;
463
+ html += \`<span class="sep">/</span><span onclick="zoomTo('\${p}')">\${part}</span>\`;
464
+ }
465
+ el.innerHTML = html;
466
+ }
467
+
468
+ function setFilter(f) {
469
+ currentFilter = f;
470
+ document.querySelectorAll('#controls button').forEach(btn => {
471
+ btn.classList.toggle('active', btn.textContent.toLowerCase().includes(f));
472
+ });
473
+ render();
474
+ }
475
+
476
+ window.addEventListener('resize', render);
477
+ render();
478
+ </script>
479
+ </body>
480
+ </html>`;
481
+ }
482
+ async function generateAndOpenHTML(result, repoPath) {
483
+ const html = generateTreemapHTML(result);
484
+ const outputPath = join(repoPath, "gitfamiliar-report.html");
485
+ writeFileSync(outputPath, html, "utf-8");
486
+ console.log(`Report generated: ${outputPath}`);
487
+ await openBrowser(outputPath);
488
+ }
489
+
490
+ // src/cli/index.ts
491
+ function createProgram() {
492
+ const program2 = new Command();
493
+ program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version("0.1.0").option("-m, --mode <mode>", "Scoring mode: binary, authorship, review-coverage, weighted", "binary").option("-u, --user <user>", "Git user name or email (defaults to git config)").option("-f, --filter <filter>", "Filter mode: all, written, reviewed", "all").option("-e, --expiration <policy>", "Expiration policy: never, time:180d, change:50%, combined:365d:50%", "never").option("--html", "Generate HTML treemap report", false).option("-w, --weights <weights>", 'Weights for weighted mode: blame,commit,review (e.g., "0.5,0.35,0.15")').action(async (rawOptions) => {
494
+ try {
495
+ const repoPath = process.cwd();
496
+ const options = parseOptions(rawOptions, repoPath);
497
+ const result = await computeFamiliarity(options);
498
+ if (options.html) {
499
+ await generateAndOpenHTML(result, repoPath);
500
+ } else {
501
+ renderTerminal(result);
502
+ }
503
+ } catch (error) {
504
+ console.error(`Error: ${error.message}`);
505
+ process.exit(1);
506
+ }
507
+ });
508
+ return program2;
509
+ }
510
+
511
+ // bin/gitfamiliar.ts
512
+ var program = createProgram();
513
+ program.parse();
514
+ //# sourceMappingURL=gitfamiliar.js.map