gitfamiliar 0.4.0 → 0.7.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/README.md CHANGED
@@ -7,7 +7,6 @@
7
7
  <a href="https://www.npmjs.com/package/gitfamiliar"><img src="https://img.shields.io/npm/v/gitfamiliar.svg" alt="npm version"></a>
8
8
  <a href="https://www.npmjs.com/package/gitfamiliar"><img src="https://img.shields.io/npm/dm/gitfamiliar.svg" alt="npm downloads"></a>
9
9
  <a href="https://github.com/kuze/gitfamiliar/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/gitfamiliar.svg" alt="license"></a>
10
- <a href="https://github.com/kuze/gitfamiliar/actions"><img src="https://github.com/kuze/gitfamiliar/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
10
  </p>
12
11
  </p>
13
12
 
@@ -35,7 +34,7 @@ Overall: 58/172 files (34%)
35
34
  tests/ ░░░░░░░░░░ 0% (0/14 files)
36
35
  config/ ██████░░░░ 60% (3/5 files)
37
36
 
38
- Written: 42 files | Reviewed: 23 files | Both: 7 files
37
+ Written: 42 files
39
38
  ```
40
39
 
41
40
  Use `--html` to generate an interactive treemap in the browser:
@@ -45,7 +44,7 @@ $ npx gitfamiliar --html
45
44
  ```
46
45
 
47
46
  > Area = lines of code, Color = familiarity (red -> green).
48
- > Click folders to drill down. Toggle between All / Written / Reviewed views.
47
+ > Click folders to drill down.
49
48
 
50
49
  ## Quick Start
51
50
 
@@ -68,31 +67,21 @@ npm install -g gitfamiliar
68
67
  | What it measures | How much you **wrote** | How well you **understand** |
69
68
  | Metric | Lines / commits (cumulative) | Familiarity score (multi-signal) |
70
69
  | Use case | Contribution stats | Onboarding progress |
71
- | Review awareness | No | Yes (PR reviews count) |
72
70
  | Time decay | No | Yes (knowledge fades) |
71
+ | Team analysis | No | Yes (bus factor, multi-user comparison) |
73
72
 
74
73
  ## Scoring Modes
75
74
 
76
- GitFamiliar provides 4 modes so you can choose the right lens for your situation.
77
-
78
75
  ### Binary (default)
79
76
 
80
- Files are "read" or "unread". A file counts as read if you committed to it or approved a PR containing it.
77
+ Files are "written" or "not written". A file counts as written if you have at least one commit touching it.
81
78
 
82
79
  ```
83
- familiarity = read_files / total_files
80
+ familiarity = written_files / total_files
84
81
  ```
85
82
 
86
- | View | Shows |
87
- |---|---|
88
- | All (default) | Written + Reviewed files |
89
- | Written only | Only files you committed to |
90
- | Reviewed only | Only files you reviewed via PR |
91
-
92
83
  ```bash
93
- gitfamiliar # All
94
- gitfamiliar --filter written # Written only
95
- gitfamiliar --filter reviewed # Reviewed only
84
+ gitfamiliar # default
96
85
  ```
97
86
 
98
87
  > Best for: **New team members** tracking onboarding progress.
@@ -112,41 +101,24 @@ gitfamiliar --mode authorship
112
101
  ```
113
102
 
114
103
  > Best for: **Tech leads** assessing bus factor and code ownership.
115
- > A file where one person owns 95% of the lines is a risk signal.
116
-
117
- ### Review Coverage
118
-
119
- Files you reviewed through PR approvals or comments, excluding your own commits.
120
-
121
- ```
122
- score = reviewed_files / total_files
123
- ```
124
-
125
- ```bash
126
- gitfamiliar --mode review-coverage
127
- ```
128
-
129
- > Best for: **Senior engineers** tracking how broadly they review.
130
- > Requires a GitHub token (see [GitHub Integration](#github-integration)).
131
104
 
132
105
  ### Weighted
133
106
 
134
- The most nuanced mode. Combines three signals with configurable weights and time decay:
107
+ Combines two signals with configurable weights and time decay:
135
108
 
136
109
  ```
137
- familiarity = w1 x blame_score + w2 x commit_score + w3 x review_score
110
+ familiarity = w1 x blame_score + w2 x commit_score
138
111
  ```
139
112
 
140
- Default weights: `blame=0.5, commit=0.35, review=0.15`
113
+ Default weights: `blame=0.5, commit=0.5`
141
114
 
142
115
  Key features:
143
116
  - **Sigmoid normalization** prevents a single large commit from dominating
144
117
  - **Recency decay** (half-life: 180 days) models knowledge fading over time
145
- - **Scope factor** discounts reviews on huge PRs (attention dilution)
146
118
 
147
119
  ```bash
148
120
  gitfamiliar --mode weighted
149
- gitfamiliar --mode weighted --weights "0.6,0.3,0.1" # custom weights
121
+ gitfamiliar --mode weighted --weights "0.6,0.4" # custom weights
150
122
  ```
151
123
 
152
124
  <details>
@@ -162,31 +134,65 @@ commit_score:
162
134
  commit 2 (45 days ago, +5/-2): sigmoid(6/200) x decay(45d) = 0.09 x 0.84
163
135
  total: min(1, 0.39) = 0.39
164
136
 
165
- review_score:
166
- PR approved (20 days ago, 4 files): 0.30 x 1.0 x decay(20d) = 0.28
167
-
168
- familiarity = 0.5 x 0.15 + 0.35 x 0.39 + 0.15 x 0.28
169
- = 0.075 + 0.137 + 0.042
170
- = 0.254 -> 25%
137
+ familiarity = 0.5 x 0.15 + 0.5 x 0.39
138
+ = 0.075 + 0.195
139
+ = 0.27 -> 27%
171
140
  ```
172
141
 
173
142
  </details>
174
143
 
175
144
  > Best for: **Power users** who want the most accurate picture.
176
- > Score breakdowns are always visible so it never feels like a black box.
145
+
146
+ ## Team Features
147
+
148
+ ### Multi-User Comparison
149
+
150
+ Compare familiarity across multiple team members:
151
+
152
+ ```bash
153
+ gitfamiliar --user "Alice" --user "Bob" # specific users
154
+ gitfamiliar --team # all contributors
155
+ ```
156
+
157
+ ### Team Coverage Map
158
+
159
+ Visualize bus factor — how many people know each part of the codebase:
160
+
161
+ ```bash
162
+ gitfamiliar --team-coverage
163
+ gitfamiliar --team-coverage --html
164
+ ```
165
+
166
+ Shows risk levels per folder:
167
+ - **RISK** (0-1 contributors) — single point of failure
168
+ - **MODERATE** (2-3 contributors) — some coverage
169
+ - **SAFE** (4+ contributors) — well-distributed knowledge
170
+
171
+ ### Hotspot Analysis
172
+
173
+ Find files that are frequently changed but poorly understood:
174
+
175
+ ```bash
176
+ gitfamiliar --hotspot # personal hotspots
177
+ gitfamiliar --hotspot team # team hotspots
178
+ gitfamiliar --hotspot --window 30 # last 30 days only
179
+ gitfamiliar --hotspot --html # scatter plot visualization
180
+ ```
181
+
182
+ Risk = high change frequency x low familiarity.
177
183
 
178
184
  ## Expiration Policies
179
185
 
180
- By default, "read" status never expires. But real knowledge fades. Configure expiration to keep scores honest:
186
+ By default, "written" status never expires. But real knowledge fades. Configure expiration to keep scores honest:
181
187
 
182
188
  | Policy | Flag | What happens |
183
189
  |---|---|---|
184
- | Never | `--expiration never` | Once read, always read (default) |
190
+ | Never | `--expiration never` | Once written, always counted (default) |
185
191
  | Time-based | `--expiration time:180d` | Expires 180 days after your last touch |
186
192
  | Change-based | `--expiration change:50%` | Expires if 50%+ of the file changed since you last touched it |
187
193
  | Combined | `--expiration combined:365d:50%` | Expires if **either** condition is met |
188
194
 
189
- The change-based policy is the smartest: it detects when the code you read has been substantially rewritten, meaning your understanding is likely outdated.
195
+ The change-based policy is the smartest: it detects when the code you wrote has been substantially rewritten, meaning your understanding is likely outdated.
190
196
 
191
197
  ## File Filtering
192
198
 
@@ -210,20 +216,6 @@ third_party/
210
216
  **/migrations/
211
217
  ```
212
218
 
213
- ## GitHub Integration
214
-
215
- For review-related features (Review Coverage mode, reviewed files in Binary mode), GitFamiliar needs a GitHub token:
216
-
217
- ```bash
218
- # Option 1: environment variable
219
- export GITHUB_TOKEN=ghp_xxxxxxxxxxxxx
220
-
221
- # Option 2: GitHub CLI (auto-detected if installed)
222
- gh auth login
223
- ```
224
-
225
- Without a token, GitFamiliar works perfectly for all non-review features. Review features degrade gracefully with a helpful message.
226
-
227
219
  ## CLI Reference
228
220
 
229
221
  ```
@@ -231,15 +223,18 @@ Usage: gitfamiliar [options]
231
223
 
232
224
  Options:
233
225
  -m, --mode <mode> Scoring mode (default: "binary")
234
- Choices: binary, authorship, review-coverage, weighted
235
- -u, --user <user> Git user name or email (default: git config)
236
- -f, --filter <filter> Display filter (default: "all")
237
- Choices: all, written, reviewed
226
+ Choices: binary, authorship, weighted
227
+ -u, --user <user> Git user name or email (repeatable for comparison)
228
+ Default: git config user.name
238
229
  -e, --expiration <policy> Expiration policy (default: "never")
239
230
  Examples: time:180d, change:50%, combined:365d:50%
240
231
  --html Generate interactive HTML treemap report
241
- -w, --weights <weights> Weights for weighted mode: blame,commit,review
242
- Example: "0.5,0.35,0.15" (must sum to 1.0)
232
+ -w, --weights <weights> Weights for weighted mode: blame,commit
233
+ Example: "0.6,0.4" (must sum to 1.0)
234
+ --team Compare all contributors
235
+ --team-coverage Show team coverage map (bus factor analysis)
236
+ --hotspot [mode] Hotspot analysis: personal (default) or team
237
+ --window <days> Time window for hotspot analysis (default: 90)
243
238
  -V, --version Output version number
244
239
  -h, --help Display help
245
240
  ```
@@ -253,9 +248,8 @@ import { computeFamiliarity } from 'gitfamiliar';
253
248
 
254
249
  const result = await computeFamiliarity({
255
250
  mode: 'binary',
256
- filter: 'all',
257
251
  expiration: { policy: 'never' },
258
- weights: { blame: 0.5, commit: 0.35, review: 0.15 },
252
+ weights: { blame: 0.5, commit: 0.5 },
259
253
  html: false,
260
254
  repoPath: '/path/to/repo',
261
255
  });
@@ -267,7 +261,6 @@ console.log(`Score: ${Math.round(result.tree.score * 100)}%`);
267
261
 
268
262
  - **Node.js** >= 18
269
263
  - **Git** (available in PATH)
270
- - **GitHub token** (optional, for review features)
271
264
 
272
265
  ## Contributing
273
266
 
@@ -284,8 +277,6 @@ npm test
284
277
  ## Roadmap
285
278
 
286
279
  - [ ] **Dependency Awareness** - Factor in understanding of imported files
287
- - [ ] **Churn Risk Alert** - Highlight files with high change frequency + low familiarity
288
- - [ ] **GitHub Action** - Post familiarity reports as PR comments
289
280
  - [ ] **VS Code Extension** - See familiarity scores inline in the editor
290
281
  - [ ] **README Badge** - Codecov-style badge for your project README
291
282
 
@@ -1,16 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  GitClient,
4
- GitHubClient,
5
4
  buildFileTree,
6
5
  computeFamiliarity,
7
6
  createFilter,
8
7
  parseExpirationConfig,
9
8
  processBatch,
10
- resolveGitHubToken,
11
9
  resolveUser,
12
10
  walkFiles
13
- } from "../chunk-V27FX6R6.js";
11
+ } from "../chunk-BQCHOSLA.js";
14
12
 
15
13
  // src/cli/index.ts
16
14
  import { Command } from "commander";
@@ -18,8 +16,7 @@ import { Command } from "commander";
18
16
  // src/core/types.ts
19
17
  var DEFAULT_WEIGHTS = {
20
18
  blame: 0.5,
21
- commit: 0.35,
22
- review: 0.15
19
+ commit: 0.5
23
20
  };
24
21
  var DEFAULT_EXPIRATION = {
25
22
  policy: "never"
@@ -28,7 +25,6 @@ var DEFAULT_EXPIRATION = {
28
25
  // src/cli/options.ts
29
26
  function parseOptions(raw, repoPath) {
30
27
  const mode = validateMode(raw.mode || "binary");
31
- const filter = validateFilter(raw.filter || "all");
32
28
  let weights = DEFAULT_WEIGHTS;
33
29
  if (raw.weights) {
34
30
  weights = parseWeights(raw.weights);
@@ -52,7 +48,6 @@ function parseOptions(raw, repoPath) {
52
48
  return {
53
49
  mode,
54
50
  user,
55
- filter,
56
51
  expiration,
57
52
  html: raw.html || false,
58
53
  weights,
@@ -60,18 +55,11 @@ function parseOptions(raw, repoPath) {
60
55
  team: raw.team || false,
61
56
  teamCoverage: raw.teamCoverage || false,
62
57
  hotspot,
63
- window: windowDays,
64
- githubUrl: raw.githubUrl,
65
- checkGithub: raw.checkGithub || false
58
+ window: windowDays
66
59
  };
67
60
  }
68
61
  function validateMode(mode) {
69
- const valid = [
70
- "binary",
71
- "authorship",
72
- "review-coverage",
73
- "weighted"
74
- ];
62
+ const valid = ["binary", "authorship", "weighted"];
75
63
  if (!valid.includes(mode)) {
76
64
  throw new Error(
77
65
  `Invalid mode: "${mode}". Valid modes: ${valid.join(", ")}`
@@ -79,27 +67,16 @@ function validateMode(mode) {
79
67
  }
80
68
  return mode;
81
69
  }
82
- function validateFilter(filter) {
83
- const valid = ["all", "written", "reviewed"];
84
- if (!valid.includes(filter)) {
85
- throw new Error(
86
- `Invalid filter: "${filter}". Valid filters: ${valid.join(", ")}`
87
- );
88
- }
89
- return filter;
90
- }
91
70
  function parseWeights(s) {
92
71
  const parts = s.split(",").map(Number);
93
- if (parts.length !== 3 || parts.some(isNaN)) {
94
- throw new Error(
95
- `Invalid weights: "${s}". Expected format: "0.5,0.35,0.15"`
96
- );
72
+ if (parts.length !== 2 || parts.some(isNaN)) {
73
+ throw new Error(`Invalid weights: "${s}". Expected format: "0.5,0.5"`);
97
74
  }
98
- const sum = parts[0] + parts[1] + parts[2];
75
+ const sum = parts[0] + parts[1];
99
76
  if (Math.abs(sum - 1) > 0.01) {
100
77
  throw new Error(`Weights must sum to 1.0, got ${sum}`);
101
78
  }
102
- return { blame: parts[0], commit: parts[1], review: parts[2] };
79
+ return { blame: parts[0], commit: parts[1] };
103
80
  }
104
81
 
105
82
  // src/cli/output/terminal.ts
@@ -125,8 +102,6 @@ function getModeLabel(mode) {
125
102
  return "Binary mode";
126
103
  case "authorship":
127
104
  return "Authorship mode";
128
- case "review-coverage":
129
- return "Review Coverage mode";
130
105
  case "weighted":
131
106
  return "Weighted mode";
132
107
  default:
@@ -166,7 +141,9 @@ function renderFolder(node, indent, mode, maxDepth) {
166
141
  function renderTerminal(result) {
167
142
  const { tree, repoName, mode } = result;
168
143
  console.log("");
169
- console.log(chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`));
144
+ console.log(
145
+ chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
146
+ );
170
147
  console.log("");
171
148
  if (mode === "binary") {
172
149
  const readCount = tree.readCount || 0;
@@ -183,10 +160,8 @@ function renderTerminal(result) {
183
160
  }
184
161
  console.log("");
185
162
  if (mode === "binary") {
186
- const { writtenCount, reviewedCount, bothCount } = result;
187
- console.log(
188
- `Written: ${writtenCount} files | Reviewed: ${reviewedCount} files | Both: ${bothCount} files`
189
- );
163
+ const { writtenCount } = result;
164
+ console.log(`Written: ${writtenCount} files`);
190
165
  console.log("");
191
166
  }
192
167
  }
@@ -244,28 +219,7 @@ function generateTreemapHTML(result) {
244
219
  #breadcrumb span { cursor: pointer; color: #5eadf7; }
245
220
  #breadcrumb span:hover { text-decoration: underline; }
246
221
  #breadcrumb .sep { color: #666; margin: 0 4px; }
247
- #controls {
248
- padding: 8px 24px;
249
- background: #16213e;
250
- border-bottom: 1px solid #0f3460;
251
- display: flex;
252
- gap: 12px;
253
- align-items: center;
254
- }
255
- #controls button {
256
- padding: 4px 12px;
257
- border: 1px solid #0f3460;
258
- background: #1a1a2e;
259
- color: #e0e0e0;
260
- border-radius: 4px;
261
- cursor: pointer;
262
- font-size: 12px;
263
- }
264
- #controls button.active {
265
- background: #e94560;
266
- border-color: #e94560;
267
- color: white;
268
- }
222
+
269
223
  #treemap { width: 100%; }
270
224
  #tooltip {
271
225
  position: absolute;
@@ -306,13 +260,7 @@ function generateTreemapHTML(result) {
306
260
  <div class="info">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>
307
261
  </div>
308
262
  <div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
309
- ${mode === "binary" ? `
310
- <div id="controls">
311
- <span style="font-size:12px;color:#888;">Filter:</span>
312
- <button class="active" onclick="setFilter('all')">All</button>
313
- <button onclick="setFilter('written')">Written only</button>
314
- <button onclick="setFilter('reviewed')">Reviewed only</button>
315
- </div>` : ""}
263
+
316
264
  <div id="treemap"></div>
317
265
  <div id="tooltip"></div>
318
266
  <div id="legend">
@@ -325,7 +273,6 @@ ${mode === "binary" ? `
325
273
  <script>
326
274
  const rawData = ${dataJson};
327
275
  const mode = "${mode}";
328
- let currentFilter = 'all';
329
276
  let currentPath = '';
330
277
 
331
278
  function scoreColor(score) {
@@ -340,9 +287,6 @@ function scoreColor(score) {
340
287
  }
341
288
 
342
289
  function getNodeScore(node) {
343
- if (mode !== 'binary') return node.score;
344
- if (currentFilter === 'written') return node.isWritten ? 1 : 0;
345
- if (currentFilter === 'reviewed') return node.isReviewed ? 1 : 0;
346
290
  return node.score;
347
291
  }
348
292
 
@@ -382,10 +326,8 @@ function render() {
382
326
 
383
327
  const headerH = document.getElementById('header').offsetHeight;
384
328
  const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;
385
- const controlsEl = document.getElementById('controls');
386
- const controlsH = controlsEl ? controlsEl.offsetHeight : 0;
387
329
  const width = window.innerWidth;
388
- const height = window.innerHeight - headerH - breadcrumbH - controlsH;
330
+ const height = window.innerHeight - headerH - breadcrumbH;
389
331
 
390
332
  const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;
391
333
  if (!targetNode) return;
@@ -492,9 +434,7 @@ function showTooltip(data, event) {
492
434
  if (data.commitScore !== undefined) {
493
435
  html += '<br>Commit: ' + Math.round(data.commitScore * 100) + '%';
494
436
  }
495
- if (data.reviewScore !== undefined) {
496
- html += '<br>Review: ' + Math.round(data.reviewScore * 100) + '%';
497
- }
437
+
498
438
  if (data.isExpired) {
499
439
  html += '<br><span style="color:#e94560">Expired</span>';
500
440
  }
@@ -523,14 +463,6 @@ function updateBreadcrumb() {
523
463
  el.innerHTML = html;
524
464
  }
525
465
 
526
- function setFilter(f) {
527
- currentFilter = f;
528
- document.querySelectorAll('#controls button').forEach(btn => {
529
- btn.classList.toggle('active', btn.textContent.toLowerCase().includes(f));
530
- });
531
- render();
532
- }
533
-
534
466
  window.addEventListener('resize', render);
535
467
  render();
536
468
  </script>
@@ -1162,7 +1094,6 @@ async function computeMultiUser(options) {
1162
1094
  const userSummaries = results.map((r) => ({
1163
1095
  user: { name: r.result.userName, email: "" },
1164
1096
  writtenCount: r.result.writtenCount,
1165
- reviewedCount: r.result.reviewedCount,
1166
1097
  overallScore: r.result.tree.score
1167
1098
  }));
1168
1099
  return {
@@ -1199,8 +1130,7 @@ function mergeResults(results) {
1199
1130
  scores.push({
1200
1131
  user: { name: userName, email: "" },
1201
1132
  score: file.score,
1202
- isWritten: file.isWritten,
1203
- isReviewed: file.isReviewed
1133
+ isWritten: file.isWritten
1204
1134
  });
1205
1135
  });
1206
1136
  }
@@ -1275,8 +1205,6 @@ function getModeLabel2(mode) {
1275
1205
  return "Binary mode";
1276
1206
  case "authorship":
1277
1207
  return "Authorship mode";
1278
- case "review-coverage":
1279
- return "Review Coverage mode";
1280
1208
  case "weighted":
1281
1209
  return "Weighted mode";
1282
1210
  default:
@@ -1323,7 +1251,7 @@ function renderMultiUserTerminal(result) {
1323
1251
  const pct = formatPercent2(summary.overallScore);
1324
1252
  if (mode === "binary") {
1325
1253
  console.log(
1326
- ` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount + summary.reviewedCount}/${totalFiles} files)`
1254
+ ` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`
1327
1255
  );
1328
1256
  } else {
1329
1257
  console.log(` ${name} ${bar} ${pct.padStart(4)}`);
@@ -1332,9 +1260,7 @@ function renderMultiUserTerminal(result) {
1332
1260
  console.log("");
1333
1261
  const nameWidth = 20;
1334
1262
  const headerNames = userSummaries.map((s) => truncateName(s.user.name, 7).padStart(7)).join(" ");
1335
- console.log(
1336
- chalk3.bold("Folders:") + " ".repeat(nameWidth - 4) + headerNames
1337
- );
1263
+ console.log(chalk3.bold("Folders:") + " ".repeat(nameWidth - 4) + headerNames);
1338
1264
  const folderLines = renderFolder3(tree, 1, 2, nameWidth);
1339
1265
  for (const line of folderLines) {
1340
1266
  console.log(line);
@@ -2149,58 +2075,6 @@ async function generateAndOpenHotspotHTML(result, repoPath) {
2149
2075
  await openBrowser(outputPath);
2150
2076
  }
2151
2077
 
2152
- // src/github/check.ts
2153
- async function checkGitHubConnection(repoPath, githubUrl) {
2154
- const gitClient = new GitClient(repoPath);
2155
- if (!await gitClient.isRepo()) {
2156
- console.error("Error: Not a git repository.");
2157
- process.exit(1);
2158
- }
2159
- const remoteUrl = await gitClient.getRemoteUrl();
2160
- if (!remoteUrl) {
2161
- console.error("Error: No git remote found.");
2162
- process.exit(1);
2163
- }
2164
- console.log(`Remote URL: ${remoteUrl}`);
2165
- const parsed = GitHubClient.parseRemoteUrl(remoteUrl, githubUrl);
2166
- if (!parsed) {
2167
- console.error("Error: Could not parse remote URL as a GitHub repository.");
2168
- process.exit(1);
2169
- }
2170
- console.log(`Hostname: ${parsed.hostname}`);
2171
- console.log(`Repository: ${parsed.owner}/${parsed.repo}`);
2172
- console.log(`API Base URL: ${parsed.apiBaseUrl}`);
2173
- console.log(`
2174
- Resolving token for hostname: ${parsed.hostname}`);
2175
- const token = resolveGitHubToken(parsed.hostname);
2176
- if (!token) {
2177
- console.error(
2178
- `No GitHub token found.
2179
- Tried:
2180
- 1. Environment variables: GITHUB_TOKEN, GH_TOKEN
2181
- 2. gh auth token --hostname ${parsed.hostname}
2182
- ` + (parsed.hostname !== "github.com" ? ` 3. gh auth token (default host fallback)
2183
- ` : "") + `
2184
- Please run: gh auth login` + (parsed.hostname !== "github.com" ? ` --hostname ${parsed.hostname}` : "")
2185
- );
2186
- process.exit(1);
2187
- }
2188
- console.log(`Token: ****${token.slice(-4)}`);
2189
- console.log("\nVerifying API connectivity...");
2190
- try {
2191
- const client = new GitHubClient(token, parsed.apiBaseUrl);
2192
- const user = await client.verifyConnection();
2193
- console.log(
2194
- `Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`
2195
- );
2196
- console.log("\nGitHub connection OK.");
2197
- } catch (error) {
2198
- console.error(`
2199
- API connection failed: ${error.message}`);
2200
- process.exit(1);
2201
- }
2202
- }
2203
-
2204
2078
  // src/cli/index.ts
2205
2079
  function collect(value, previous) {
2206
2080
  return previous.concat([value]);
@@ -2209,24 +2083,20 @@ function createProgram() {
2209
2083
  const program2 = new Command();
2210
2084
  program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version("0.1.1").option(
2211
2085
  "-m, --mode <mode>",
2212
- "Scoring mode: binary, authorship, review-coverage, weighted",
2086
+ "Scoring mode: binary, authorship, weighted",
2213
2087
  "binary"
2214
2088
  ).option(
2215
2089
  "-u, --user <user>",
2216
2090
  "Git user name or email (repeatable for comparison)",
2217
2091
  collect,
2218
2092
  []
2219
- ).option(
2220
- "-f, --filter <filter>",
2221
- "Filter mode: all, written, reviewed",
2222
- "all"
2223
2093
  ).option(
2224
2094
  "-e, --expiration <policy>",
2225
2095
  "Expiration policy: never, time:180d, change:50%, combined:365d:50%",
2226
2096
  "never"
2227
2097
  ).option("--html", "Generate HTML treemap report", false).option(
2228
2098
  "-w, --weights <weights>",
2229
- 'Weights for weighted mode: blame,commit,review (e.g., "0.5,0.35,0.15")'
2099
+ 'Weights for weighted mode: blame,commit (e.g., "0.5,0.5")'
2230
2100
  ).option("--team", "Compare all contributors", false).option(
2231
2101
  "--team-coverage",
2232
2102
  "Show team coverage map (bus factor analysis)",
@@ -2234,21 +2104,10 @@ function createProgram() {
2234
2104
  ).option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option(
2235
2105
  "--window <days>",
2236
2106
  "Time window for hotspot analysis in days (default: 90)"
2237
- ).option(
2238
- "--github-url <hostname>",
2239
- "GitHub Enterprise hostname (e.g. ghe.example.com). Auto-detected from git remote if omitted."
2240
- ).option(
2241
- "--check-github",
2242
- "Verify GitHub API connectivity and show connection info",
2243
- false
2244
2107
  ).action(async (rawOptions) => {
2245
2108
  try {
2246
2109
  const repoPath = process.cwd();
2247
2110
  const options = parseOptions(rawOptions, repoPath);
2248
- if (options.checkGithub) {
2249
- await checkGitHubConnection(repoPath, options.githubUrl);
2250
- return;
2251
- }
2252
2111
  if (options.hotspot) {
2253
2112
  const result2 = await computeHotspots(options);
2254
2113
  if (options.html) {