gimpact 0.1.2

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 (4) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +22 -0
  3. package/dist/index.js +2061 -0
  4. package/package.json +38 -0
package/dist/index.js ADDED
@@ -0,0 +1,2061 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import * as p from "@clack/prompts";
5
+ import chalk7 from "chalk";
6
+
7
+ // src/core/analysis/filters/file-pattern-filter.ts
8
+ import { simpleGit } from "simple-git";
9
+ var DEFAULT_EXCLUDE_PATTERNS = [
10
+ // Lock files
11
+ "**/*.lock",
12
+ "**/package-lock.json",
13
+ "**/yarn.lock",
14
+ "**/pnpm-lock.yaml",
15
+ "**/uv.lock",
16
+ "**/Cargo.lock",
17
+ "**/Gemfile.lock",
18
+ "**/Pipfile.lock",
19
+ "**/poetry.lock",
20
+ "**/composer.lock",
21
+ "**/go.sum",
22
+ "**/go.mod",
23
+ // Build outputs
24
+ "**/node_modules/**",
25
+ "**/dist/**",
26
+ "**/build/**",
27
+ "**/out/**",
28
+ "**/.next/**",
29
+ "**/.nuxt/**",
30
+ "**/.cache/**",
31
+ "**/coverage/**",
32
+ "**/.coverage/**",
33
+ // Generated files
34
+ "**/openapi.json",
35
+ "**/openapi.yaml",
36
+ "**/openapi.yml",
37
+ "**/*.generated.*",
38
+ "**/*.pb.go",
39
+ "**/*.pb.ts",
40
+ "**/*.pb.js",
41
+ // IDE and editor files
42
+ "**/.idea/**",
43
+ "**/.vscode/**",
44
+ "**/.DS_Store",
45
+ "**/Thumbs.db",
46
+ // Logs and temporary files
47
+ "**/*.log",
48
+ "**/*.tmp",
49
+ "**/*.temp"
50
+ ];
51
+ function matchesPattern(filePath, patterns) {
52
+ for (const pattern of patterns) {
53
+ if (matchesGlob(filePath, pattern)) {
54
+ return true;
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+ function matchesGlob(filePath, pattern) {
60
+ const normalizedPath = filePath.replace(/^\.\//, "");
61
+ let normalizedPattern = pattern.replace(/^\.\//, "");
62
+ normalizedPattern = normalizedPattern.replace(/\*\*/g, "\0RECURSIVE\0");
63
+ let regexPattern = normalizedPattern.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".");
64
+ regexPattern = regexPattern.replace(/\0RECURSIVE\0/g, ".*?");
65
+ const originalPattern = pattern.replace(/^\.\//, "");
66
+ if (!originalPattern.startsWith("/") && !originalPattern.startsWith("**")) {
67
+ regexPattern = `.*?${regexPattern}`;
68
+ }
69
+ try {
70
+ const regex = new RegExp(`^${regexPattern}$`);
71
+ return regex.test(normalizedPath);
72
+ } catch (_error) {
73
+ return false;
74
+ }
75
+ }
76
+ async function areGitIgnored(filePaths, git) {
77
+ if (filePaths.length === 0) {
78
+ return /* @__PURE__ */ new Set();
79
+ }
80
+ try {
81
+ const result = await git.checkIgnore(filePaths);
82
+ return new Set(result);
83
+ } catch {
84
+ return /* @__PURE__ */ new Set();
85
+ }
86
+ }
87
+ var FilePatternFilter = class {
88
+ excludePatterns;
89
+ includeDirectory;
90
+ respectGitignore;
91
+ repoRoot;
92
+ gitClient = null;
93
+ constructor(excludePatterns = [], respectGitignore = true, repoRoot = process.cwd(), includeDirectory) {
94
+ this.excludePatterns = [...DEFAULT_EXCLUDE_PATTERNS, ...excludePatterns];
95
+ this.respectGitignore = respectGitignore;
96
+ this.repoRoot = repoRoot;
97
+ this.includeDirectory = includeDirectory;
98
+ }
99
+ /**
100
+ * Get or create git client instance
101
+ */
102
+ getGitClient() {
103
+ if (!this.gitClient) {
104
+ this.gitClient = simpleGit(this.repoRoot);
105
+ }
106
+ return this.gitClient;
107
+ }
108
+ /**
109
+ * Check if a file should be excluded (pattern matching only, no git check)
110
+ */
111
+ shouldExcludeByPattern(filePath) {
112
+ if (this.includeDirectory) {
113
+ const normalizedDir = this.includeDirectory.replace(/^\.\//, "").replace(/\/$/, "");
114
+ const normalizedPath = filePath.replace(/^\.\//, "");
115
+ const isInDirectory = normalizedPath === normalizedDir || normalizedPath.startsWith(`${normalizedDir}/`);
116
+ if (!isInDirectory) {
117
+ return true;
118
+ }
119
+ }
120
+ return matchesPattern(filePath, this.excludePatterns);
121
+ }
122
+ /**
123
+ * Filter ownership analysis result
124
+ */
125
+ async filter(result) {
126
+ const filteredFiles = {};
127
+ const filteredDirectories = {};
128
+ const filteredAuthors = {};
129
+ const filesToCheckGitIgnore = [];
130
+ const fileOwnershipMap = /* @__PURE__ */ new Map();
131
+ for (const [filePath, fileOwnership] of Object.entries(result.files)) {
132
+ if (this.shouldExcludeByPattern(filePath)) {
133
+ continue;
134
+ }
135
+ if (this.respectGitignore) {
136
+ filesToCheckGitIgnore.push(filePath);
137
+ fileOwnershipMap.set(filePath, fileOwnership);
138
+ } else {
139
+ filteredFiles[filePath] = fileOwnership;
140
+ }
141
+ }
142
+ if (this.respectGitignore && filesToCheckGitIgnore.length > 0) {
143
+ const gitClient = this.getGitClient();
144
+ const ignoredFiles = await areGitIgnored(filesToCheckGitIgnore, gitClient);
145
+ for (const filePath of filesToCheckGitIgnore) {
146
+ if (!ignoredFiles.has(filePath)) {
147
+ const fileOwnership = fileOwnershipMap.get(filePath);
148
+ if (fileOwnership) {
149
+ filteredFiles[filePath] = fileOwnership;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ const directoryFileCounts = {};
155
+ for (const [filePath, fileOwnership] of Object.entries(filteredFiles)) {
156
+ const directory = this.getDirectory(filePath);
157
+ if (!directoryFileCounts[directory]) {
158
+ directoryFileCounts[directory] = {};
159
+ }
160
+ const owner = fileOwnership.owner;
161
+ directoryFileCounts[directory][owner] = (directoryFileCounts[directory][owner] || 0) + 1;
162
+ }
163
+ for (const [directory, ownerCounts] of Object.entries(directoryFileCounts)) {
164
+ if (this.includeDirectory) {
165
+ const normalizedDir = this.includeDirectory.replace(/^\.\//, "").replace(/\/$/, "");
166
+ const normalizedDirectory = directory.replace(/^\.\//, "").replace(/\/$/, "");
167
+ if (!normalizedDirectory.startsWith(`${normalizedDir}/`) && normalizedDirectory !== normalizedDir) {
168
+ continue;
169
+ }
170
+ }
171
+ let primaryOwner = "";
172
+ let maxFiles = 0;
173
+ for (const [owner, count] of Object.entries(ownerCounts)) {
174
+ if (count > maxFiles) {
175
+ maxFiles = count;
176
+ primaryOwner = owner;
177
+ }
178
+ }
179
+ if (primaryOwner) {
180
+ const totalFiles = Object.values(ownerCounts).reduce((sum, count) => sum + count, 0);
181
+ const totalLines = Object.entries(filteredFiles).filter(([path]) => this.getDirectory(path) === directory).reduce((sum, [, ownership]) => sum + ownership.totalLines, 0);
182
+ filteredDirectories[directory] = {
183
+ directory,
184
+ owner: primaryOwner,
185
+ share: totalFiles > 0 ? Math.round(maxFiles / totalFiles * 100) : 0,
186
+ ownerFiles: maxFiles,
187
+ totalFiles,
188
+ totalLines
189
+ };
190
+ }
191
+ }
192
+ for (const [filePath, fileOwnership] of Object.entries(filteredFiles)) {
193
+ const owner = fileOwnership.owner;
194
+ if (!filteredAuthors[owner]) {
195
+ filteredAuthors[owner] = {
196
+ author: owner,
197
+ files: [],
198
+ totalFiles: 0,
199
+ totalLines: 0
200
+ };
201
+ }
202
+ filteredAuthors[owner].files.push({
203
+ file: filePath,
204
+ lines: fileOwnership.ownerLines,
205
+ share: fileOwnership.share
206
+ });
207
+ filteredAuthors[owner].totalFiles++;
208
+ filteredAuthors[owner].totalLines += fileOwnership.ownerLines;
209
+ }
210
+ for (const author of Object.values(filteredAuthors)) {
211
+ author.files.sort((a, b) => b.lines - a.lines);
212
+ }
213
+ return {
214
+ files: filteredFiles,
215
+ directories: filteredDirectories,
216
+ authors: filteredAuthors
217
+ };
218
+ }
219
+ getDirectory(filePath) {
220
+ const lastSlashIndex = filePath.lastIndexOf("/");
221
+ if (lastSlashIndex === -1) {
222
+ return "./";
223
+ }
224
+ return filePath.substring(0, lastSlashIndex + 1);
225
+ }
226
+ };
227
+
228
+ // src/core/analysis/filters/filter-chain.ts
229
+ var FilterChain = class {
230
+ filters = [];
231
+ addFilter(filter) {
232
+ this.filters.push(filter);
233
+ return this;
234
+ }
235
+ apply(data) {
236
+ return this.filters.reduce((result, filter) => filter.apply(result), data);
237
+ }
238
+ isEmpty() {
239
+ return this.filters.length === 0;
240
+ }
241
+ };
242
+
243
+ // src/core/analysis/filters/min-commits-filter.ts
244
+ var MinCommitsFilter = class {
245
+ constructor(minCommits) {
246
+ this.minCommits = minCommits;
247
+ }
248
+ apply(result) {
249
+ if (Array.isArray(result)) {
250
+ return result.filter((item) => item.stats.commits >= this.minCommits);
251
+ } else {
252
+ const filteredStats = {};
253
+ const filteredEfficiency = {};
254
+ for (const [author, authorStats] of Object.entries(result.stats)) {
255
+ if (authorStats.commits >= this.minCommits) {
256
+ filteredStats[author] = authorStats;
257
+ if (result.efficiency[author]) {
258
+ filteredEfficiency[author] = result.efficiency[author];
259
+ }
260
+ }
261
+ }
262
+ return {
263
+ stats: filteredStats,
264
+ efficiency: filteredEfficiency
265
+ };
266
+ }
267
+ }
268
+ };
269
+
270
+ // src/core/analysis/log-parsers/aggregate-log-parser.ts
271
+ function parseAggregateStats(log, authors) {
272
+ const stats = {};
273
+ const authorFiles = {};
274
+ const lines = log.split("\n");
275
+ let currentAuthor = "";
276
+ const authorFilter = authors?.map((a) => a.toLowerCase());
277
+ for (const line of lines) {
278
+ const trimmedLine = line.trim();
279
+ if (trimmedLine.length === 0) {
280
+ continue;
281
+ }
282
+ if (trimmedLine.includes(" ")) {
283
+ const parts = trimmedLine.split(" ");
284
+ const insertions = parseInt(parts[0], 10) || 0;
285
+ const deletions = parseInt(parts[1], 10) || 0;
286
+ const filePath = parts[2];
287
+ if (currentAuthor && stats[currentAuthor]) {
288
+ stats[currentAuthor].insertions += insertions;
289
+ stats[currentAuthor].deletions += deletions;
290
+ if (filePath) {
291
+ authorFiles[currentAuthor].add(filePath);
292
+ }
293
+ }
294
+ } else {
295
+ currentAuthor = trimmedLine;
296
+ if (authorFilter && !authorFilter.includes(currentAuthor.toLowerCase())) {
297
+ currentAuthor = "";
298
+ continue;
299
+ }
300
+ if (!stats[currentAuthor]) {
301
+ stats[currentAuthor] = {
302
+ commits: 0,
303
+ insertions: 0,
304
+ deletions: 0,
305
+ filesTouched: 0
306
+ };
307
+ authorFiles[currentAuthor] = /* @__PURE__ */ new Set();
308
+ }
309
+ stats[currentAuthor].commits++;
310
+ }
311
+ }
312
+ for (const author of Object.keys(stats)) {
313
+ stats[author].filesTouched = authorFiles[author]?.size ?? 0;
314
+ }
315
+ return stats;
316
+ }
317
+
318
+ // src/core/analysis/log-parsers/efficiency-log-parser.ts
319
+ function parseEfficiencyLog(log) {
320
+ const commits = [];
321
+ const lines = log.split("\n");
322
+ let currentCommit = null;
323
+ for (const line of lines) {
324
+ const trimmedLine = line.trim();
325
+ if (trimmedLine.length === 0) {
326
+ continue;
327
+ }
328
+ if (trimmedLine.includes("|") && !trimmedLine.includes(" ")) {
329
+ if (currentCommit) {
330
+ commits.push(currentCommit);
331
+ }
332
+ const parts = trimmedLine.split("|");
333
+ if (parts.length >= 2) {
334
+ currentCommit = {
335
+ author: parts[0],
336
+ date: new Date(parts[1]),
337
+ insertions: 0,
338
+ deletions: 0,
339
+ files: []
340
+ };
341
+ }
342
+ } else if (trimmedLine.includes(" ") && currentCommit) {
343
+ const parts = trimmedLine.split(" ");
344
+ const insertions = parseInt(parts[0], 10) || 0;
345
+ const deletions = parseInt(parts[1], 10) || 0;
346
+ const filePath = parts[2];
347
+ currentCommit.insertions += insertions;
348
+ currentCommit.deletions += deletions;
349
+ if (filePath) {
350
+ currentCommit.files.push(filePath);
351
+ }
352
+ }
353
+ }
354
+ if (currentCommit) {
355
+ commits.push(currentCommit);
356
+ }
357
+ return commits;
358
+ }
359
+
360
+ // src/core/analysis/log-parsers/ownership-log-parser.ts
361
+ function parseOwnershipStats(log, authors) {
362
+ const fileAuthorLines = {};
363
+ const fileLastCommitDates = {};
364
+ const lines = log.split("\n");
365
+ let currentAuthor = "";
366
+ let currentCommitDate = null;
367
+ const authorFilter = authors?.map((a) => a.toLowerCase());
368
+ for (const line of lines) {
369
+ const trimmedLine = line.trim();
370
+ if (trimmedLine.length === 0) {
371
+ continue;
372
+ }
373
+ if (trimmedLine.startsWith("AUTHOR:")) {
374
+ const authorPart = trimmedLine.replace("AUTHOR:", "").trim();
375
+ if (authorPart.includes("|DATE:")) {
376
+ const parts = authorPart.split("|DATE:");
377
+ currentAuthor = parts[0].trim();
378
+ const dateStr = parts[1]?.trim();
379
+ if (dateStr) {
380
+ currentCommitDate = new Date(dateStr);
381
+ if (Number.isNaN(currentCommitDate.getTime())) {
382
+ currentCommitDate = null;
383
+ }
384
+ } else {
385
+ currentCommitDate = null;
386
+ }
387
+ } else {
388
+ currentAuthor = authorPart;
389
+ currentCommitDate = null;
390
+ }
391
+ fetch("http://127.0.0.1:7242/ingest/f771081e-8bc3-4743-9457-44201273bd79", {
392
+ method: "POST",
393
+ headers: { "Content-Type": "application/json" },
394
+ body: JSON.stringify({
395
+ location: "ownership-log-parser.ts:30",
396
+ message: "Author line parsed",
397
+ data: { currentAuthor, isEmpty: !currentAuthor, length: currentAuthor.length },
398
+ timestamp: Date.now(),
399
+ sessionId: "debug-session",
400
+ runId: "run1",
401
+ hypothesisId: "B"
402
+ })
403
+ }).catch(() => {
404
+ });
405
+ if (authorFilter && !authorFilter.includes(currentAuthor.toLowerCase())) {
406
+ currentAuthor = "";
407
+ }
408
+ } else if (trimmedLine.includes(" ") && currentAuthor) {
409
+ const parts = trimmedLine.split(" ");
410
+ if (parts.length >= 3) {
411
+ const insertions = parseInt(parts[0], 10) || 0;
412
+ const deletions = parseInt(parts[1], 10) || 0;
413
+ let filePath = parts[2];
414
+ if (!filePath) {
415
+ continue;
416
+ }
417
+ const originalFilePath = filePath;
418
+ filePath = normalizeRenameNotation(filePath);
419
+ fetch("http://127.0.0.1:7242/ingest/f771081e-8bc3-4743-9457-44201273bd79", {
420
+ method: "POST",
421
+ headers: { "Content-Type": "application/json" },
422
+ body: JSON.stringify({
423
+ location: "ownership-log-parser.ts:42",
424
+ message: "File path parsed",
425
+ data: {
426
+ originalFilePath,
427
+ normalizedFilePath: filePath,
428
+ hasRenameNotation: originalFilePath.includes("{") && originalFilePath.includes("=>"),
429
+ currentAuthor,
430
+ isEmpty: !currentAuthor
431
+ },
432
+ timestamp: Date.now(),
433
+ sessionId: "debug-session",
434
+ runId: "run1",
435
+ hypothesisId: "A"
436
+ })
437
+ }).catch(() => {
438
+ });
439
+ if (!filePath) {
440
+ continue;
441
+ }
442
+ const linesChanged = insertions + deletions;
443
+ if (!fileAuthorLines[filePath]) {
444
+ fileAuthorLines[filePath] = {};
445
+ }
446
+ fileAuthorLines[filePath][currentAuthor] = (fileAuthorLines[filePath][currentAuthor] || 0) + linesChanged;
447
+ if (currentCommitDate) {
448
+ const existingDate = fileLastCommitDates[filePath];
449
+ if (!existingDate || currentCommitDate > existingDate) {
450
+ fileLastCommitDates[filePath] = currentCommitDate;
451
+ }
452
+ }
453
+ }
454
+ }
455
+ }
456
+ const fileOwnership = {};
457
+ const directoryOwnership = {};
458
+ const authorOwnership = {};
459
+ for (const [file, authorLines] of Object.entries(fileAuthorLines)) {
460
+ const totalLines = Object.values(authorLines).reduce((sum, lines2) => sum + lines2, 0);
461
+ let primaryOwner = "";
462
+ let maxLines = -1;
463
+ for (const [author, lines2] of Object.entries(authorLines)) {
464
+ if (!author || author.trim() === "") {
465
+ continue;
466
+ }
467
+ if (lines2 > maxLines) {
468
+ maxLines = lines2;
469
+ primaryOwner = author;
470
+ }
471
+ }
472
+ if (!primaryOwner) {
473
+ continue;
474
+ }
475
+ fetch("http://127.0.0.1:7242/ingest/f771081e-8bc3-4743-9457-44201273bd79", {
476
+ method: "POST",
477
+ headers: { "Content-Type": "application/json" },
478
+ body: JSON.stringify({
479
+ location: "ownership-log-parser.ts:77",
480
+ message: "Primary owner calculated",
481
+ data: {
482
+ file,
483
+ primaryOwner,
484
+ isEmpty: !primaryOwner,
485
+ hasRenameNotation: file.includes("{ => }"),
486
+ authorKeys: Object.keys(authorLines)
487
+ },
488
+ timestamp: Date.now(),
489
+ sessionId: "debug-session",
490
+ runId: "run1",
491
+ hypothesisId: "C"
492
+ })
493
+ }).catch(() => {
494
+ });
495
+ const share = totalLines > 0 ? Math.round(maxLines / totalLines * 100) : 0;
496
+ fileOwnership[file] = {
497
+ file,
498
+ owner: primaryOwner,
499
+ share,
500
+ ownerLines: maxLines,
501
+ totalLines,
502
+ authors: { ...authorLines },
503
+ lastCommitDate: fileLastCommitDates[file]
504
+ };
505
+ const directory = getDirectory(file);
506
+ fetch("http://127.0.0.1:7242/ingest/f771081e-8bc3-4743-9457-44201273bd79", {
507
+ method: "POST",
508
+ headers: { "Content-Type": "application/json" },
509
+ body: JSON.stringify({
510
+ location: "ownership-log-parser.ts:94",
511
+ message: "Directory extracted",
512
+ data: { file, directory, hasRenameNotation: directory.includes("{ => }") },
513
+ timestamp: Date.now(),
514
+ sessionId: "debug-session",
515
+ runId: "run1",
516
+ hypothesisId: "D"
517
+ })
518
+ }).catch(() => {
519
+ });
520
+ if (!directoryOwnership[directory]) {
521
+ directoryOwnership[directory] = {};
522
+ }
523
+ directoryOwnership[directory][file] = maxLines;
524
+ if (!authorOwnership[primaryOwner]) {
525
+ authorOwnership[primaryOwner] = {
526
+ author: primaryOwner,
527
+ files: [],
528
+ totalFiles: 0,
529
+ totalLines: 0
530
+ };
531
+ }
532
+ authorOwnership[primaryOwner].files.push({
533
+ file,
534
+ lines: maxLines,
535
+ share
536
+ });
537
+ authorOwnership[primaryOwner].totalFiles++;
538
+ authorOwnership[primaryOwner].totalLines += maxLines;
539
+ }
540
+ const directoryResult = {};
541
+ for (const [directory, files] of Object.entries(directoryOwnership)) {
542
+ const fileList = Object.keys(files);
543
+ const totalFiles = fileList.length;
544
+ const authorFileCounts = {};
545
+ for (const file of fileList) {
546
+ const ownership = fileOwnership[file];
547
+ if (ownership) {
548
+ authorFileCounts[ownership.owner] = (authorFileCounts[ownership.owner] || 0) + 1;
549
+ }
550
+ }
551
+ let primaryOwner = "";
552
+ let maxFiles = 0;
553
+ for (const [author, count] of Object.entries(authorFileCounts)) {
554
+ if (!author || author.trim() === "") {
555
+ continue;
556
+ }
557
+ if (count > maxFiles) {
558
+ maxFiles = count;
559
+ primaryOwner = author;
560
+ }
561
+ }
562
+ if (!primaryOwner) {
563
+ continue;
564
+ }
565
+ fetch("http://127.0.0.1:7242/ingest/f771081e-8bc3-4743-9457-44201273bd79", {
566
+ method: "POST",
567
+ headers: { "Content-Type": "application/json" },
568
+ body: JSON.stringify({
569
+ location: "ownership-log-parser.ts:140",
570
+ message: "Directory owner calculated",
571
+ data: {
572
+ directory,
573
+ primaryOwner,
574
+ isEmpty: !primaryOwner,
575
+ hasRenameNotation: directory.includes("{ => }"),
576
+ authorFileCounts
577
+ },
578
+ timestamp: Date.now(),
579
+ sessionId: "debug-session",
580
+ runId: "run1",
581
+ hypothesisId: "C"
582
+ })
583
+ }).catch(() => {
584
+ });
585
+ const share = totalFiles > 0 ? Math.round(maxFiles / totalFiles * 100) : 0;
586
+ const totalLines = fileList.reduce(
587
+ (sum, file) => sum + (fileOwnership[file]?.totalLines || 0),
588
+ 0
589
+ );
590
+ directoryResult[directory] = {
591
+ directory,
592
+ owner: primaryOwner,
593
+ share,
594
+ ownerFiles: maxFiles,
595
+ totalFiles,
596
+ totalLines
597
+ };
598
+ }
599
+ for (const author of Object.values(authorOwnership)) {
600
+ author.files.sort((a, b) => b.lines - a.lines);
601
+ }
602
+ return {
603
+ files: fileOwnership,
604
+ directories: directoryResult,
605
+ authors: authorOwnership
606
+ };
607
+ }
608
+ function normalizeRenameNotation(filePath) {
609
+ const renamePattern = /\{[^}]*\s*=>\s*([^}]*)\}/g;
610
+ let result = filePath;
611
+ let hasEmptyRename = false;
612
+ result = result.replace(renamePattern, (_match, newPath) => {
613
+ const normalized = newPath.trim();
614
+ if (!normalized) {
615
+ hasEmptyRename = true;
616
+ return "";
617
+ }
618
+ return normalized;
619
+ });
620
+ if (hasEmptyRename && result.startsWith("/")) {
621
+ if (result === "/" || result.match(/^\/[^/]*$/)) {
622
+ return "";
623
+ }
624
+ }
625
+ return result;
626
+ }
627
+ function getDirectory(filePath) {
628
+ const lastSlashIndex = filePath.lastIndexOf("/");
629
+ if (lastSlashIndex === -1) {
630
+ return "./";
631
+ }
632
+ return filePath.substring(0, lastSlashIndex + 1);
633
+ }
634
+
635
+ // src/core/time/formatter.ts
636
+ function formatDateForGit(date) {
637
+ return date.toISOString().split("T")[0];
638
+ }
639
+
640
+ // src/core/time/period-identifier.ts
641
+ function getWeekNumber(date) {
642
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
643
+ const dayNum = d.getUTCDay() || 7;
644
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
645
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
646
+ const weekNo = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
647
+ return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
648
+ }
649
+ function getMonthIdentifier(date) {
650
+ const year = date.getFullYear();
651
+ const month = String(date.getMonth() + 1).padStart(2, "0");
652
+ return `${year}-${month}`;
653
+ }
654
+ function getPeriodIdentifier(date, periodUnit) {
655
+ switch (periodUnit) {
656
+ case "daily":
657
+ return formatDateForGit(date);
658
+ case "weekly":
659
+ return getWeekNumber(date);
660
+ case "monthly":
661
+ return getMonthIdentifier(date);
662
+ }
663
+ }
664
+
665
+ // src/core/time/validator.ts
666
+ function validateTimeRange(since, until) {
667
+ if (since && until && since > until) {
668
+ throw new Error("--since date must be before --until date");
669
+ }
670
+ const now = /* @__PURE__ */ new Date();
671
+ if (since && since > now) {
672
+ throw new Error("--since date cannot be in the future");
673
+ }
674
+ if (until && until > now) {
675
+ throw new Error("--until date cannot be in the future");
676
+ }
677
+ }
678
+
679
+ // src/core/analysis/log-parsers/period-log-parser.ts
680
+ function parsePeriodStats(log, periodUnit, authors) {
681
+ const grouped = /* @__PURE__ */ new Map();
682
+ const lines = log.split("\n");
683
+ let currentAuthor = "";
684
+ let currentPeriod = "";
685
+ const authorFilter = authors?.map((a) => a.toLowerCase());
686
+ for (const line of lines) {
687
+ const trimmedLine = line.trim();
688
+ if (trimmedLine.length === 0) {
689
+ continue;
690
+ }
691
+ if (trimmedLine.includes(" ")) {
692
+ const parts = trimmedLine.split(" ");
693
+ const insertions = parseInt(parts[0], 10) || 0;
694
+ const deletions = parseInt(parts[1], 10) || 0;
695
+ if (currentAuthor && currentPeriod) {
696
+ if (!grouped.has(currentPeriod)) {
697
+ grouped.set(currentPeriod, /* @__PURE__ */ new Map());
698
+ }
699
+ const periodMap = grouped.get(currentPeriod);
700
+ if (!periodMap.has(currentAuthor)) {
701
+ periodMap.set(currentAuthor, {
702
+ commits: 0,
703
+ insertions: 0,
704
+ deletions: 0,
705
+ filesTouched: 0
706
+ });
707
+ }
708
+ const stats = periodMap.get(currentAuthor);
709
+ stats.insertions += insertions;
710
+ stats.deletions += deletions;
711
+ stats.filesTouched++;
712
+ }
713
+ } else {
714
+ const parts = trimmedLine.split("|");
715
+ if (parts.length === 2) {
716
+ currentAuthor = parts[0];
717
+ const dateStr = parts[1];
718
+ const commitDate = new Date(dateStr);
719
+ if (authorFilter && !authorFilter.includes(currentAuthor.toLowerCase())) {
720
+ currentAuthor = "";
721
+ currentPeriod = "";
722
+ continue;
723
+ }
724
+ currentPeriod = getPeriodIdentifier(commitDate, periodUnit);
725
+ if (!grouped.has(currentPeriod)) {
726
+ grouped.set(currentPeriod, /* @__PURE__ */ new Map());
727
+ }
728
+ const periodMap = grouped.get(currentPeriod);
729
+ if (!periodMap.has(currentAuthor)) {
730
+ periodMap.set(currentAuthor, {
731
+ commits: 0,
732
+ insertions: 0,
733
+ deletions: 0,
734
+ filesTouched: 0
735
+ });
736
+ }
737
+ const stats = periodMap.get(currentAuthor);
738
+ stats.commits++;
739
+ }
740
+ }
741
+ }
742
+ return convertToSortedArray(grouped);
743
+ }
744
+ function convertToSortedArray(grouped) {
745
+ const result = [];
746
+ for (const [period, authorMap] of grouped.entries()) {
747
+ for (const [author, stats] of authorMap.entries()) {
748
+ result.push({ period, author, stats });
749
+ }
750
+ }
751
+ result.sort((a, b) => {
752
+ if (a.period !== b.period) {
753
+ return b.period.localeCompare(a.period);
754
+ }
755
+ const impactA = a.stats.insertions + a.stats.deletions;
756
+ const impactB = b.stats.insertions + b.stats.deletions;
757
+ return impactB - impactA;
758
+ });
759
+ return result;
760
+ }
761
+
762
+ // src/core/constants/analysis-mode.constants.ts
763
+ var DEFAULT_MODE = "aggregate";
764
+ var VALID_MODES = ["aggregate", "periodic", "ownership"];
765
+
766
+ // src/core/constants/efficiency-thresholds.constants.ts
767
+ var EFFICIENCY_THRESHOLDS = {
768
+ /** Below this is considered "Micro Commits" */
769
+ MICRO: 10,
770
+ /** Good range lower bound */
771
+ OPTIMAL_MIN: 30,
772
+ /** Good range upper bound */
773
+ OPTIMAL_MAX: 150,
774
+ /** Above this is considered "Huge" */
775
+ HUGE: 500
776
+ };
777
+
778
+ // src/core/constants/period-unit.constants.ts
779
+ var VALID_PERIOD_UNITS = ["daily", "weekly", "monthly"];
780
+ var DEFAULT_PERIOD_UNIT = "daily";
781
+
782
+ // src/core/constants/index.ts
783
+ var DEFAULT_DAYS = 30;
784
+
785
+ // src/core/analysis/metrics/efficiency-metric.ts
786
+ function getEfficiencyLabel(efficiency) {
787
+ if (efficiency < EFFICIENCY_THRESHOLDS.MICRO) {
788
+ return "\u{1F7E1} Micro";
789
+ }
790
+ if (efficiency >= EFFICIENCY_THRESHOLDS.OPTIMAL_MIN && efficiency <= EFFICIENCY_THRESHOLDS.OPTIMAL_MAX) {
791
+ return "\u2705 Optimal";
792
+ }
793
+ if (efficiency > EFFICIENCY_THRESHOLDS.HUGE) {
794
+ return "\u{1F6A8} Huge";
795
+ }
796
+ if (efficiency > EFFICIENCY_THRESHOLDS.OPTIMAL_MAX) {
797
+ return "\u26A0\uFE0F High Load";
798
+ }
799
+ return "\u{1F7E1} Small";
800
+ }
801
+ function classifyCommitSize(linesChanged) {
802
+ if (linesChanged <= EFFICIENCY_THRESHOLDS.MICRO) {
803
+ return "micro";
804
+ }
805
+ if (linesChanged < EFFICIENCY_THRESHOLDS.OPTIMAL_MIN) {
806
+ return "small";
807
+ }
808
+ if (linesChanged <= EFFICIENCY_THRESHOLDS.OPTIMAL_MAX) {
809
+ return "optimal";
810
+ }
811
+ if (linesChanged <= EFFICIENCY_THRESHOLDS.HUGE) {
812
+ return "high";
813
+ }
814
+ return "huge";
815
+ }
816
+ function analyzeAuthorEfficiency(authorName, commits) {
817
+ const authorCommits = commits.filter((c) => c.author === authorName);
818
+ const distribution = {
819
+ micro: 0,
820
+ small: 0,
821
+ optimal: 0,
822
+ high: 0,
823
+ huge: 0
824
+ };
825
+ if (authorCommits.length === 0) {
826
+ return {
827
+ author: authorName,
828
+ efficiency: 0,
829
+ efficiencyLabel: getEfficiencyLabel(0),
830
+ distribution,
831
+ totalCommits: 0
832
+ };
833
+ }
834
+ let totalDelta = 0;
835
+ for (const commit of authorCommits) {
836
+ const linesChanged = commit.insertions + commit.deletions;
837
+ totalDelta += linesChanged;
838
+ const bucket = classifyCommitSize(linesChanged);
839
+ distribution[bucket]++;
840
+ }
841
+ const efficiency = Math.round(totalDelta / authorCommits.length);
842
+ return {
843
+ author: authorName,
844
+ efficiency,
845
+ efficiencyLabel: getEfficiencyLabel(efficiency),
846
+ distribution,
847
+ totalCommits: authorCommits.length
848
+ };
849
+ }
850
+ function analyzeAllEfficiency(commits) {
851
+ const authors = /* @__PURE__ */ new Set();
852
+ for (const commit of commits) {
853
+ authors.add(commit.author);
854
+ }
855
+ const result = {};
856
+ for (const author of authors) {
857
+ result[author] = analyzeAuthorEfficiency(author, commits);
858
+ }
859
+ return result;
860
+ }
861
+
862
+ // src/core/config/query-builder.ts
863
+ function buildLogQuery(opts) {
864
+ const query = {
865
+ branch: opts.branch
866
+ };
867
+ if (opts.timeRange.since || opts.timeRange.until || opts.timeRange.days) {
868
+ if (opts.timeRange.since) {
869
+ validateTimeRange(opts.timeRange.since, opts.timeRange.until);
870
+ query.since = formatDateForGit(opts.timeRange.since);
871
+ } else if (opts.timeRange.days) {
872
+ query.since = `${opts.timeRange.days} days ago`;
873
+ }
874
+ if (opts.timeRange.until) {
875
+ query.until = formatDateForGit(opts.timeRange.until);
876
+ }
877
+ }
878
+ return query;
879
+ }
880
+
881
+ // src/core/config/resolver.ts
882
+ function resolveOptions(options) {
883
+ return {
884
+ timeRange: {
885
+ ...{ days: DEFAULT_DAYS },
886
+ ...options.timeRange
887
+ },
888
+ mode: options.mode ?? DEFAULT_MODE,
889
+ periodUnit: options.periodUnit ?? DEFAULT_PERIOD_UNIT,
890
+ branch: options.branch
891
+ };
892
+ }
893
+
894
+ // src/core/analysis/modes/aggregate-mode.ts
895
+ var AggregateMode = class {
896
+ async handle(options, gitClient, originalOptions) {
897
+ const { since, until, branch } = buildLogQuery(options);
898
+ let aggregateLog = "";
899
+ let efficiencyLog = "";
900
+ try {
901
+ [aggregateLog, efficiencyLog] = await Promise.all([
902
+ gitClient.getAggregateLog(since, until, branch),
903
+ gitClient.getStabilityLog(since, until, branch)
904
+ ]);
905
+ } catch (error) {
906
+ if (error instanceof Error && error.message.includes("does not have any commits")) {
907
+ return {
908
+ stats: {},
909
+ efficiency: {}
910
+ };
911
+ }
912
+ throw error;
913
+ }
914
+ const stats = parseAggregateStats(aggregateLog, originalOptions.authors);
915
+ const commits = parseEfficiencyLog(efficiencyLog);
916
+ const authorFilter = originalOptions.authors?.map((a) => a.toLowerCase());
917
+ const filteredCommits = authorFilter ? commits.filter((c) => authorFilter.includes(c.author.toLowerCase())) : commits;
918
+ const efficiency = analyzeAllEfficiency(filteredCommits);
919
+ return {
920
+ stats,
921
+ efficiency
922
+ };
923
+ }
924
+ };
925
+
926
+ // src/core/analysis/modes/ownership-mode.ts
927
+ var OwnershipMode = class {
928
+ async handle(options, gitClient, originalOptions) {
929
+ const { since, until, branch } = buildLogQuery(options);
930
+ let fileAuthorLog = "";
931
+ try {
932
+ fileAuthorLog = await gitClient.getFileAuthorLog(
933
+ since,
934
+ until,
935
+ branch,
936
+ originalOptions.directory
937
+ );
938
+ } catch (error) {
939
+ if (error instanceof Error && error.message.includes("does not have any commits")) {
940
+ return {
941
+ files: {},
942
+ directories: {},
943
+ authors: {}
944
+ };
945
+ }
946
+ throw error;
947
+ }
948
+ const result = parseOwnershipStats(fileAuthorLog, originalOptions.authors);
949
+ return result;
950
+ }
951
+ };
952
+
953
+ // src/core/analysis/modes/periodic-mode.ts
954
+ var PeriodicMode = class {
955
+ async handle(options, gitClient, originalOptions) {
956
+ const { since, until, branch } = buildLogQuery(options);
957
+ let log = "";
958
+ try {
959
+ log = await gitClient.getPeriodicLog(since, until, branch);
960
+ } catch (error) {
961
+ if (error instanceof Error && error.message.includes("does not have any commits")) {
962
+ return [];
963
+ }
964
+ throw error;
965
+ }
966
+ return parsePeriodStats(log, options.periodUnit, originalOptions.authors);
967
+ }
968
+ };
969
+
970
+ // src/core/analysis/modes/mode-registry.ts
971
+ var modeRegistry = /* @__PURE__ */ new Map([
972
+ ["aggregate", new AggregateMode()],
973
+ ["periodic", new PeriodicMode()],
974
+ ["ownership", new OwnershipMode()]
975
+ ]);
976
+
977
+ // src/core/git/client.ts
978
+ import { simpleGit as simpleGit2 } from "simple-git";
979
+ var GitClientImpl = class {
980
+ git;
981
+ constructor(git) {
982
+ this.git = git ?? simpleGit2();
983
+ }
984
+ async isRepository() {
985
+ return this.git.checkIsRepo();
986
+ }
987
+ /**
988
+ * Get aggregate stats log (author name only)
989
+ * Used for total contribution analysis
990
+ */
991
+ async getAggregateLog(since, until, branch) {
992
+ const args = ["log", "--numstat", "--pretty=format:%aN"];
993
+ if (branch) args.push(branch);
994
+ if (since) args.push(`--since=${since}`);
995
+ if (until) args.push(`--until=${until}`);
996
+ return this.git.raw(args);
997
+ }
998
+ /**
999
+ * Get periodic stats log (author name with commit date)
1000
+ * Used for time-based grouped analysis
1001
+ */
1002
+ async getPeriodicLog(since, until, branch) {
1003
+ const args = ["log", "--numstat", "--pretty=format:%aN|%cd", "--date=iso"];
1004
+ if (branch) args.push(branch);
1005
+ if (since) args.push(`--since=${since}`);
1006
+ if (until) args.push(`--until=${until}`);
1007
+ return this.git.raw(args);
1008
+ }
1009
+ /**
1010
+ * Get stability analysis log (author name with commit date and file changes)
1011
+ * Used for efficiency and churn rate analysis
1012
+ * Format: author|date followed by numstat lines
1013
+ */
1014
+ async getStabilityLog(since, until, branch) {
1015
+ const args = ["log", "--numstat", "--pretty=format:%aN|%cd", "--date=iso", "--reverse"];
1016
+ if (branch) args.push(branch);
1017
+ if (since) args.push(`--since=${since}`);
1018
+ if (until) args.push(`--until=${until}`);
1019
+ return this.git.raw(args);
1020
+ }
1021
+ /**
1022
+ * Get file-author log with numstat (author name followed by file changes with line counts)
1023
+ * Used for ownership analysis
1024
+ * Format: AUTHOR:<name>|DATE:<date> followed by numstat lines (insertions\tdeletions\tfilepath)
1025
+ * The AUTHOR: prefix ensures author names are not confused with file paths
1026
+ */
1027
+ async getFileAuthorLog(since, until, branch, directory) {
1028
+ const args = ["log", "--numstat", "--pretty=format:AUTHOR:%aN|DATE:%cd", "--date=iso"];
1029
+ if (branch) args.push(branch);
1030
+ if (since) {
1031
+ if (since.includes("days ago") || since.includes("day ago")) {
1032
+ args.push(`--since=${since}`);
1033
+ } else {
1034
+ args.push(`--since=${since}`);
1035
+ }
1036
+ }
1037
+ if (until) {
1038
+ if (until.includes("days ago") || until.includes("day ago")) {
1039
+ args.push(`--until=${until}`);
1040
+ } else {
1041
+ args.push(`--until=${until}`);
1042
+ }
1043
+ }
1044
+ if (directory) {
1045
+ args.push("--");
1046
+ args.push(directory);
1047
+ }
1048
+ return this.git.raw(args);
1049
+ }
1050
+ /**
1051
+ * Get commit-files log (commit hash followed by file paths)
1052
+ * Used for temporal coupling analysis (files changed together)
1053
+ * Format: COMMIT:<hash> followed by file paths (one per line)
1054
+ */
1055
+ async getCommitFilesLog(since, until, branch) {
1056
+ const args = ["log", "--name-only", "--pretty=format:COMMIT:%H"];
1057
+ if (branch) args.push(branch);
1058
+ if (since) args.push(`--since=${since}`);
1059
+ if (until) args.push(`--until=${until}`);
1060
+ return this.git.raw(args);
1061
+ }
1062
+ };
1063
+
1064
+ // src/core/git/client-factory.ts
1065
+ function createGitClient(git) {
1066
+ return new GitClientImpl(git);
1067
+ }
1068
+
1069
+ // src/core/analysis/orchestrator.ts
1070
+ async function analyzeContributions(options = {}, gitClient) {
1071
+ const client = gitClient ?? createGitClient();
1072
+ const isRepo = await client.isRepository();
1073
+ if (!isRepo) {
1074
+ throw new Error("Not a git repository. Please run this command in a git repository.");
1075
+ }
1076
+ const resolvedOpts = resolveOptions(options);
1077
+ const mode = modeRegistry.get(resolvedOpts.mode);
1078
+ if (!mode) {
1079
+ throw new Error(`Unknown analysis mode: ${resolvedOpts.mode}`);
1080
+ }
1081
+ let result = await mode.handle(resolvedOpts, client, {
1082
+ authors: options.authors,
1083
+ directory: options.directory
1084
+ });
1085
+ if (resolvedOpts.mode === "ownership") {
1086
+ const filePatternFilter = new FilePatternFilter(
1087
+ options.excludePatterns,
1088
+ options.respectGitignore !== false,
1089
+ // default to true
1090
+ process.cwd(),
1091
+ void 0
1092
+ // Don't filter by directory here since it's already filtered at Git log level
1093
+ );
1094
+ result = await filePatternFilter.filter(result);
1095
+ } else {
1096
+ const filterChain = new FilterChain();
1097
+ if (options.minCommits !== void 0 && options.minCommits > 1) {
1098
+ filterChain.addFilter(new MinCommitsFilter(options.minCommits));
1099
+ }
1100
+ if (!filterChain.isEmpty()) {
1101
+ result = filterChain.apply(result);
1102
+ }
1103
+ }
1104
+ return result;
1105
+ }
1106
+
1107
+ // src/cli/commands/command-builder.ts
1108
+ import { Command } from "commander";
1109
+ function addCommonOptions(command) {
1110
+ return command.option(
1111
+ "-d, --days <number>",
1112
+ `number of days to analyze (default: ${DEFAULT_DAYS})`,
1113
+ (val) => parseInt(val, 10)
1114
+ ).option("-s, --since <date>", "start date (YYYY-MM-DD)", (val) => new Date(val)).option("-u, --until <date>", "end date (YYYY-MM-DD)", (val) => new Date(val)).option(
1115
+ "-a, --authors <names...>",
1116
+ "filter by specific author names (can specify multiple). Enables deep-dive stability analysis"
1117
+ ).option("-b, --branch <name>", "analyze specific branch (default: current branch)").option(
1118
+ "-c, --min-commits <number>",
1119
+ "minimum number of commits to include an author (default: 1)",
1120
+ (val) => parseInt(val, 10)
1121
+ );
1122
+ }
1123
+ function createCommand() {
1124
+ const program = new Command();
1125
+ program.name("gimpact").description("Visualize Git contribution impact by developer").version("0.1.0").option(
1126
+ "-d, --days <number>",
1127
+ `number of days to analyze (default: ${DEFAULT_DAYS})`,
1128
+ (val) => parseInt(val, 10)
1129
+ ).option("-s, --since <date>", "start date (YYYY-MM-DD)", (val) => new Date(val)).option("-u, --until <date>", "end date (YYYY-MM-DD)", (val) => new Date(val)).option(
1130
+ "-m, --mode <type>",
1131
+ `analysis mode: ${VALID_MODES.join(", ")} (default: aggregate)`,
1132
+ "aggregate"
1133
+ ).option(
1134
+ "-p, --period-unit <type>",
1135
+ `period unit for periodic mode: ${VALID_PERIOD_UNITS.join(", ")} (default: daily)`,
1136
+ "daily"
1137
+ ).option(
1138
+ "-a, --authors <names...>",
1139
+ "filter by specific author names (can specify multiple). Enables deep-dive stability analysis"
1140
+ ).option("-b, --branch <name>", "analyze specific branch (default: current branch)").option(
1141
+ "-c, --min-commits <number>",
1142
+ "minimum number of commits to include an author (default: 1)",
1143
+ (val) => parseInt(val, 10)
1144
+ ).action(() => {
1145
+ });
1146
+ addCommonOptions(
1147
+ program.command("summary").description("Show aggregate report with all analyses (default)")
1148
+ ).action(() => {
1149
+ });
1150
+ addCommonOptions(program.command("daily").description("Show daily periodic report")).action(
1151
+ () => {
1152
+ }
1153
+ );
1154
+ addCommonOptions(program.command("weekly").description("Show weekly periodic report")).action(
1155
+ () => {
1156
+ }
1157
+ );
1158
+ addCommonOptions(program.command("monthly").description("Show monthly periodic report")).action(
1159
+ () => {
1160
+ }
1161
+ );
1162
+ addCommonOptions(
1163
+ program.command("ownership").description("Show code ownership analysis (files, directories, authors)").option(
1164
+ "--exclude-patterns <patterns...>",
1165
+ 'additional glob patterns to exclude from analysis (e.g., "**/*.lock" "**/dist/**")'
1166
+ ).option(
1167
+ "--no-respect-gitignore",
1168
+ "include files that are ignored by .gitignore (default: respect .gitignore)"
1169
+ ).option(
1170
+ "--directory <path>",
1171
+ 'filter by directory path (only include files in this directory, e.g., "packages/cli/src")'
1172
+ )
1173
+ ).action(() => {
1174
+ });
1175
+ return program;
1176
+ }
1177
+ function parseArgs(program) {
1178
+ program.parse(process.argv);
1179
+ const args = program.args;
1180
+ let subcommand = "summary";
1181
+ if (args.length > 0 && args[0]) {
1182
+ const validSubcommands = [
1183
+ "summary",
1184
+ "daily",
1185
+ "weekly",
1186
+ "monthly",
1187
+ "ownership"
1188
+ ];
1189
+ if (validSubcommands.includes(args[0])) {
1190
+ subcommand = args[0];
1191
+ }
1192
+ }
1193
+ const subcommandCommand = subcommand !== "summary" ? program.commands.find((cmd) => cmd.name() === subcommand) : null;
1194
+ const mainOpts = program.opts();
1195
+ const subOpts = subcommandCommand ? subcommandCommand.opts() : {};
1196
+ const opts = { ...mainOpts, ...subOpts };
1197
+ return {
1198
+ subcommand,
1199
+ days: opts.days,
1200
+ since: opts.since,
1201
+ until: opts.until,
1202
+ mode: opts.mode,
1203
+ periodUnit: opts.periodUnit,
1204
+ authors: opts.authors,
1205
+ branch: opts.branch,
1206
+ minCommits: opts.minCommits,
1207
+ excludePatterns: opts.excludePatterns,
1208
+ respectGitignore: opts.respectGitignore,
1209
+ directory: opts.directory
1210
+ };
1211
+ }
1212
+
1213
+ // src/cli/commands/subcommand-registry.ts
1214
+ var subcommandToMode = {
1215
+ summary: { mode: "aggregate" },
1216
+ daily: { mode: "periodic", periodUnit: "daily" },
1217
+ weekly: { mode: "periodic", periodUnit: "weekly" },
1218
+ monthly: { mode: "periodic", periodUnit: "monthly" },
1219
+ ownership: { mode: "ownership" }
1220
+ };
1221
+
1222
+ // src/cli/adapters/options-adapter.ts
1223
+ function adaptToAnalyzerOptions(options) {
1224
+ const subcommandConfig = subcommandToMode[options.subcommand];
1225
+ const mode = subcommandConfig.mode;
1226
+ const periodUnit = subcommandConfig.periodUnit ?? options.periodUnit;
1227
+ const analyzerOptions = {
1228
+ timeRange: {},
1229
+ mode,
1230
+ periodUnit,
1231
+ branch: options.branch
1232
+ };
1233
+ if (options.authors && options.authors.length > 0) {
1234
+ analyzerOptions.authors = options.authors;
1235
+ }
1236
+ if (options.since || options.until) {
1237
+ if (options.since) {
1238
+ analyzerOptions.timeRange.since = options.since;
1239
+ }
1240
+ if (options.until) {
1241
+ analyzerOptions.timeRange.until = options.until;
1242
+ }
1243
+ } else if (options.days) {
1244
+ analyzerOptions.timeRange.days = options.days;
1245
+ } else {
1246
+ analyzerOptions.timeRange.days = DEFAULT_DAYS;
1247
+ }
1248
+ if (options.minCommits !== void 0) {
1249
+ analyzerOptions.minCommits = options.minCommits;
1250
+ }
1251
+ if (options.excludePatterns && options.excludePatterns.length > 0) {
1252
+ analyzerOptions.excludePatterns = options.excludePatterns;
1253
+ }
1254
+ if (options.respectGitignore !== void 0) {
1255
+ analyzerOptions.respectGitignore = options.respectGitignore;
1256
+ }
1257
+ if (options.directory) {
1258
+ analyzerOptions.directory = options.directory;
1259
+ }
1260
+ return analyzerOptions;
1261
+ }
1262
+
1263
+ // src/cli/adapters/time-range-formatter.ts
1264
+ function buildTimeRangeDescription(options) {
1265
+ if (options.since && options.until) {
1266
+ return `${options.since.toISOString().split("T")[0]} to ${options.until.toISOString().split("T")[0]}`;
1267
+ }
1268
+ if (options.since) {
1269
+ return `Since ${options.since.toISOString().split("T")[0]}`;
1270
+ }
1271
+ if (options.until) {
1272
+ return `Until ${options.until.toISOString().split("T")[0]}`;
1273
+ }
1274
+ const days = options.days ?? DEFAULT_DAYS;
1275
+ return `Last ${days} day${days === 1 ? "" : "s"}`;
1276
+ }
1277
+
1278
+ // src/cli/output/components/bar-renderer.ts
1279
+ function renderBar(count, maxCount, maxWidth) {
1280
+ if (maxCount === 0) return "";
1281
+ const width = Math.round(count / maxCount * maxWidth);
1282
+ return "\u2593".repeat(width);
1283
+ }
1284
+
1285
+ // src/cli/output/components/summary-row.ts
1286
+ import chalk from "chalk";
1287
+ function createSummaryRow(data) {
1288
+ const row = [
1289
+ chalk.bold(`\u{1F4CA} Total (${data.timeRangeDescription})`),
1290
+ chalk.bold(chalk.cyan(data.totalCommits.toString())),
1291
+ chalk.bold(chalk.green(`+${data.totalInsertions.toLocaleString()}`)),
1292
+ chalk.bold(chalk.red(`-${data.totalDeletions.toLocaleString()}`))
1293
+ ];
1294
+ if (data.totalFiles !== void 0) {
1295
+ row.push(chalk.bold(chalk.blue(data.totalFiles.toString())));
1296
+ }
1297
+ if (data.additionalInfo) {
1298
+ row.push(chalk.bold(chalk.yellow(data.additionalInfo)));
1299
+ }
1300
+ return row;
1301
+ }
1302
+
1303
+ // src/cli/output/components/table-builder.ts
1304
+ import Table from "cli-table3";
1305
+ function createTable(config) {
1306
+ return new Table({
1307
+ head: config.head,
1308
+ colAligns: config.colAligns || [],
1309
+ style: {
1310
+ head: ["cyan"],
1311
+ border: ["gray"]
1312
+ }
1313
+ });
1314
+ }
1315
+
1316
+ // src/cli/output/printers/aggregate-printer.ts
1317
+ import chalk2 from "chalk";
1318
+ var AggregatePrinter = class {
1319
+ print(result, timeRangeDescription) {
1320
+ this.printAggregateStats(result.stats, timeRangeDescription);
1321
+ this.printEfficiencyAnalysis(result.efficiency);
1322
+ }
1323
+ printAggregateStats(stats, timeRangeDescription) {
1324
+ const sortedAuthors = Object.entries(stats).sort((a, b) => {
1325
+ const impactA = a[1].insertions + a[1].deletions;
1326
+ const impactB = b[1].insertions + b[1].deletions;
1327
+ return impactB - impactA;
1328
+ });
1329
+ if (sortedAuthors.length === 0) {
1330
+ console.log(chalk2.yellow(`
1331
+ \u26A0 No commits found in the specified time range.
1332
+ `));
1333
+ return;
1334
+ }
1335
+ const table = createTable({
1336
+ head: [
1337
+ chalk2.bold("Author"),
1338
+ chalk2.bold("Commits"),
1339
+ chalk2.bold("Insertions"),
1340
+ chalk2.bold("Deletions"),
1341
+ chalk2.bold("Files")
1342
+ ],
1343
+ colAligns: ["left", "right", "right", "right", "right"]
1344
+ });
1345
+ const totalCommits = sortedAuthors.reduce((sum, [, data]) => sum + data.commits, 0);
1346
+ const totalInsertions = sortedAuthors.reduce((sum, [, data]) => sum + data.insertions, 0);
1347
+ const totalDeletions = sortedAuthors.reduce((sum, [, data]) => sum + data.deletions, 0);
1348
+ const totalFiles = sortedAuthors.reduce((sum, [, data]) => sum + data.filesTouched, 0);
1349
+ for (const [author, data] of sortedAuthors) {
1350
+ table.push([
1351
+ author,
1352
+ chalk2.white(data.commits.toString()),
1353
+ chalk2.green(`+${data.insertions.toLocaleString()}`),
1354
+ chalk2.red(`-${data.deletions.toLocaleString()}`),
1355
+ chalk2.blue(data.filesTouched.toString())
1356
+ ]);
1357
+ }
1358
+ table.push(
1359
+ createSummaryRow({
1360
+ totalCommits,
1361
+ totalInsertions,
1362
+ totalDeletions,
1363
+ totalFiles,
1364
+ timeRangeDescription
1365
+ })
1366
+ );
1367
+ console.log(`
1368
+ ${table.toString()}
1369
+ `);
1370
+ }
1371
+ printEfficiencyAnalysis(efficiency) {
1372
+ const authors = Object.keys(efficiency);
1373
+ if (authors.length === 0) {
1374
+ return;
1375
+ }
1376
+ const totalDist = {
1377
+ micro: 0,
1378
+ small: 0,
1379
+ optimal: 0,
1380
+ high: 0,
1381
+ huge: 0
1382
+ };
1383
+ let totalCommits = 0;
1384
+ let totalLines = 0;
1385
+ for (const author of authors) {
1386
+ const stats = efficiency[author];
1387
+ totalDist.micro += stats.distribution.micro;
1388
+ totalDist.small += stats.distribution.small;
1389
+ totalDist.optimal += stats.distribution.optimal;
1390
+ totalDist.high += stats.distribution.high;
1391
+ totalDist.huge += stats.distribution.huge;
1392
+ totalCommits += stats.totalCommits;
1393
+ totalLines += stats.efficiency * stats.totalCommits;
1394
+ }
1395
+ const avgEfficiency = totalCommits > 0 ? Math.round(totalLines / totalCommits) : 0;
1396
+ console.log(chalk2.bold.cyan("\u26A1 Commit Size Distribution (All Authors)"));
1397
+ console.log(chalk2.dim(` Average: ${avgEfficiency} lines/commit`));
1398
+ console.log(chalk2.dim(` Total: ${totalCommits} commits
1399
+ `));
1400
+ const maxCount = Math.max(
1401
+ totalDist.micro,
1402
+ totalDist.small,
1403
+ totalDist.optimal,
1404
+ totalDist.high,
1405
+ totalDist.huge
1406
+ );
1407
+ const maxBarWidth = 30;
1408
+ const buckets = [
1409
+ {
1410
+ label: "Micro",
1411
+ range: "0-10",
1412
+ count: totalDist.micro,
1413
+ indicator: "\u{1F7E1}",
1414
+ color: chalk2.yellow
1415
+ },
1416
+ {
1417
+ label: "Small",
1418
+ range: "11-30",
1419
+ count: totalDist.small,
1420
+ indicator: "\u{1F7E1}",
1421
+ color: chalk2.yellow
1422
+ },
1423
+ {
1424
+ label: "Optimal",
1425
+ range: "31-150",
1426
+ count: totalDist.optimal,
1427
+ indicator: "\u2705",
1428
+ color: chalk2.green
1429
+ },
1430
+ {
1431
+ label: "High",
1432
+ range: "151-500",
1433
+ count: totalDist.high,
1434
+ indicator: "\u26A0\uFE0F",
1435
+ color: chalk2.hex("#FFA500")
1436
+ },
1437
+ { label: "Huge", range: "500+", count: totalDist.huge, indicator: "\u{1F6A8}", color: chalk2.red }
1438
+ ];
1439
+ for (const bucket of buckets) {
1440
+ const bar = renderBar(bucket.count, maxCount, maxBarWidth);
1441
+ const paddedRange = bucket.range.padStart(7);
1442
+ const countStr = bucket.count.toString().padStart(3);
1443
+ console.log(
1444
+ chalk2.gray(` ${paddedRange} `) + bucket.color(bar.padEnd(maxBarWidth)) + chalk2.gray(` ${countStr} `) + bucket.indicator
1445
+ );
1446
+ }
1447
+ console.log("");
1448
+ }
1449
+ };
1450
+
1451
+ // src/cli/output/printers/ownership-printer.ts
1452
+ import chalk5 from "chalk";
1453
+
1454
+ // src/cli/output/utils/ownership-formatter.ts
1455
+ import chalk4 from "chalk";
1456
+
1457
+ // src/cli/output/utils/tree-formatter.ts
1458
+ import chalk3 from "chalk";
1459
+ function buildDirectoryTree(files) {
1460
+ const root = {
1461
+ name: ".",
1462
+ path: ".",
1463
+ type: "directory",
1464
+ children: []
1465
+ };
1466
+ for (const [filePath, ownership] of Object.entries(files)) {
1467
+ const parts = filePath.split("/").filter((p2) => p2.length > 0);
1468
+ let current = root;
1469
+ for (let i = 0; i < parts.length; i++) {
1470
+ const part = parts[i];
1471
+ const isFile = i === parts.length - 1;
1472
+ const currentPath = parts.slice(0, i + 1).join("/");
1473
+ let node = current.children.find((child) => child.name === part);
1474
+ if (!node) {
1475
+ node = {
1476
+ name: part,
1477
+ path: currentPath,
1478
+ type: isFile ? "file" : "directory",
1479
+ children: []
1480
+ };
1481
+ current.children.push(node);
1482
+ }
1483
+ if (isFile) {
1484
+ node.owner = ownership.owner;
1485
+ node.share = ownership.share;
1486
+ node.lines = ownership.totalLines;
1487
+ node.fileOwnership = ownership;
1488
+ node.lastCommitDate = ownership.lastCommitDate;
1489
+ }
1490
+ current = node;
1491
+ }
1492
+ }
1493
+ function sortNode(node) {
1494
+ node.children.sort((a, b) => {
1495
+ if (a.type !== b.type) {
1496
+ return a.type === "directory" ? -1 : 1;
1497
+ }
1498
+ return a.name.localeCompare(b.name);
1499
+ });
1500
+ for (const child of node.children) {
1501
+ sortNode(child);
1502
+ }
1503
+ }
1504
+ sortNode(root);
1505
+ return root;
1506
+ }
1507
+ function calculateDirectoryStats(node) {
1508
+ if (node.type === "file") {
1509
+ const owner = node.owner || "";
1510
+ return {
1511
+ totalFiles: 1,
1512
+ totalLines: node.lines || 0,
1513
+ ownerCounts: owner ? { [owner]: 1 } : {}
1514
+ };
1515
+ }
1516
+ let totalFiles = 0;
1517
+ let totalLines = 0;
1518
+ const ownerCounts = {};
1519
+ let latestCommitDate;
1520
+ for (const child of node.children) {
1521
+ const stats = calculateDirectoryStats(child);
1522
+ totalFiles += stats.totalFiles;
1523
+ totalLines += stats.totalLines;
1524
+ for (const [owner, count] of Object.entries(stats.ownerCounts)) {
1525
+ ownerCounts[owner] = (ownerCounts[owner] || 0) + count;
1526
+ }
1527
+ if (child.lastCommitDate) {
1528
+ if (!latestCommitDate || child.lastCommitDate > latestCommitDate) {
1529
+ latestCommitDate = child.lastCommitDate;
1530
+ }
1531
+ }
1532
+ }
1533
+ let primaryOwner = "";
1534
+ let maxFiles = 0;
1535
+ for (const [owner, count] of Object.entries(ownerCounts)) {
1536
+ if (count > maxFiles) {
1537
+ maxFiles = count;
1538
+ primaryOwner = owner;
1539
+ }
1540
+ }
1541
+ node.owner = primaryOwner;
1542
+ node.share = totalFiles > 0 ? Math.round(maxFiles / totalFiles * 100) : 0;
1543
+ node.files = maxFiles;
1544
+ node.totalFiles = totalFiles;
1545
+ node.lines = totalLines;
1546
+ node.lastCommitDate = latestCommitDate;
1547
+ return { totalFiles, totalLines, ownerCounts };
1548
+ }
1549
+
1550
+ // src/cli/output/utils/ownership-formatter.ts
1551
+ function formatNumber(num) {
1552
+ if (num >= 1e3) {
1553
+ const k = num / 1e3;
1554
+ return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`;
1555
+ }
1556
+ return num.toString();
1557
+ }
1558
+ function getDensityIndicator(share) {
1559
+ if (share > 80) {
1560
+ return "\u2588";
1561
+ }
1562
+ if (share > 50) {
1563
+ return "\u2593";
1564
+ }
1565
+ return "\u2591";
1566
+ }
1567
+ function calculateColumnWidths(terminalWidth) {
1568
+ const minTerminalWidth = 80;
1569
+ const effectiveWidth = Math.max(terminalWidth || minTerminalWidth, minTerminalWidth);
1570
+ const treeConnectorSpace = 12;
1571
+ const pathWidth = Math.min(40, Math.floor((effectiveWidth - treeConnectorSpace) * 0.4));
1572
+ const ownerWidth = 20;
1573
+ const shareWidth = 6;
1574
+ const linesWidth = 8;
1575
+ return {
1576
+ pathWidth,
1577
+ ownerWidth,
1578
+ shareWidth,
1579
+ linesWidth
1580
+ };
1581
+ }
1582
+ function stripAnsi(str) {
1583
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
1584
+ }
1585
+ function padRight(str, width) {
1586
+ const visibleLength = stripAnsi(str).length;
1587
+ const padding = Math.max(0, width - visibleLength);
1588
+ return str + " ".repeat(padding);
1589
+ }
1590
+ function padLeft(str, width) {
1591
+ const visibleLength = stripAnsi(str).length;
1592
+ const padding = Math.max(0, width - visibleLength);
1593
+ return " ".repeat(padding) + str;
1594
+ }
1595
+ function isTestOrSnapshotPath(path) {
1596
+ const lowerPath = path.toLowerCase();
1597
+ return lowerPath.includes("__snapshots__") || lowerPath.includes(".snap") || lowerPath.includes(".test.") || lowerPath.includes(".spec.") || lowerPath.includes("__tests__") || lowerPath.includes("__test__") || lowerPath.includes("/test/") || lowerPath.includes("/tests/") || lowerPath.includes("/spec/") || lowerPath.includes("/specs/");
1598
+ }
1599
+ function shouldCollapseTrivialFolder(node) {
1600
+ return (node.lines || 0) < 100;
1601
+ }
1602
+ function flattenSingleChildPaths(node) {
1603
+ const flattened = {
1604
+ ...node,
1605
+ children: []
1606
+ };
1607
+ for (const child of node.children) {
1608
+ if (child.type === "directory" && child.children.length === 1) {
1609
+ const singleChild = child.children[0];
1610
+ if (singleChild.type === "directory") {
1611
+ const flattenedChild = flattenSingleChildPaths(singleChild);
1612
+ flattenedChild.name = `${child.name}/${singleChild.name}`;
1613
+ flattenedChild.path = singleChild.path;
1614
+ flattened.children.push(flattenedChild);
1615
+ continue;
1616
+ }
1617
+ }
1618
+ const processedChild = flattenSingleChildPaths(child);
1619
+ flattened.children.push(processedChild);
1620
+ }
1621
+ return flattened;
1622
+ }
1623
+ function calculateKnowledgeConcentrationAreas(node, concentrationAreas, directory) {
1624
+ if (node.type === "directory" && node.owner && node.share === 100 && (node.lines || 0) >= 500) {
1625
+ if (directory) {
1626
+ const normalizedDir = directory.replace(/^\.\//, "").replace(/\/$/, "");
1627
+ const normalizedPath = node.path.replace(/^\.\//, "");
1628
+ if (normalizedPath !== normalizedDir && !normalizedPath.startsWith(`${normalizedDir}/`) && !normalizedDir.startsWith(`${normalizedPath}/`)) {
1629
+ } else {
1630
+ concentrationAreas.push({
1631
+ path: node.path,
1632
+ owner: node.owner,
1633
+ share: node.share,
1634
+ lines: node.lines || 0
1635
+ });
1636
+ }
1637
+ } else {
1638
+ concentrationAreas.push({
1639
+ path: node.path,
1640
+ owner: node.owner,
1641
+ share: node.share,
1642
+ lines: node.lines || 0
1643
+ });
1644
+ }
1645
+ }
1646
+ for (const child of node.children) {
1647
+ if (child.type === "directory") {
1648
+ calculateKnowledgeConcentrationAreas(child, concentrationAreas, directory);
1649
+ }
1650
+ }
1651
+ }
1652
+ function formatRelativeTime(date) {
1653
+ const now = /* @__PURE__ */ new Date();
1654
+ const diffMs = now.getTime() - date.getTime();
1655
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1656
+ if (diffDays === 0) {
1657
+ return "today";
1658
+ }
1659
+ if (diffDays === 1) {
1660
+ return "1d ago";
1661
+ }
1662
+ if (diffDays < 7) {
1663
+ return `${diffDays}d ago`;
1664
+ }
1665
+ if (diffDays < 30) {
1666
+ const weeks = Math.floor(diffDays / 7);
1667
+ return `${weeks}w ago`;
1668
+ }
1669
+ if (diffDays < 365) {
1670
+ const months = Math.floor(diffDays / 30);
1671
+ return `${months}mo ago`;
1672
+ }
1673
+ const years = Math.floor(diffDays / 365);
1674
+ return `${years}y ago`;
1675
+ }
1676
+ function printKnowledgeConcentration(tree, columnWidths, directory) {
1677
+ const concentrationAreas = [];
1678
+ calculateKnowledgeConcentrationAreas(tree, concentrationAreas, directory);
1679
+ if (concentrationAreas.length === 0) {
1680
+ return;
1681
+ }
1682
+ concentrationAreas.sort((a, b) => b.lines - a.lines);
1683
+ const topConcentrations = concentrationAreas.slice(0, 3);
1684
+ console.log(chalk4.bold.cyan("\u{1F4CA} Top Knowledge Concentration (Density)"));
1685
+ for (let i = 0; i < topConcentrations.length; i++) {
1686
+ const concentration = topConcentrations[i];
1687
+ const displayPath = concentration.path.replace(/^\.\//, "");
1688
+ const maxPathWidth = columnWidths.pathWidth - 5;
1689
+ const truncatedPath = displayPath.length > maxPathWidth ? `${displayPath.slice(0, maxPathWidth - 3)}...` : displayPath;
1690
+ const pathDisplay = padRight(`\u{1F4C2} ${truncatedPath}`, columnWidths.pathWidth);
1691
+ const ownerDisplay = padRight(chalk4.bold(concentration.owner), columnWidths.ownerWidth);
1692
+ const shareDisplay = padLeft(`${concentration.share}%`, columnWidths.shareWidth);
1693
+ const linesDisplay = padLeft(formatNumber(concentration.lines), columnWidths.linesWidth);
1694
+ console.log(
1695
+ ` ${i + 1}. ${pathDisplay} ${chalk4.red.bold("\u2588")} ${ownerDisplay} ${chalk4.dim(`[${shareDisplay} / ${linesDisplay} lines]`)}`
1696
+ );
1697
+ }
1698
+ console.log("");
1699
+ }
1700
+ function printHeader(columnWidths) {
1701
+ const pathHeader = padRight("DIRECTORY / FOLDER", columnWidths.pathWidth);
1702
+ const ownerHeader = padRight("OWNER", columnWidths.ownerWidth);
1703
+ const shareHeader = padRight("SHARE", columnWidths.shareWidth);
1704
+ const linesHeader = padRight("LINES", columnWidths.linesWidth);
1705
+ console.log(`${pathHeader} ${ownerHeader} ${shareHeader} ${linesHeader}`);
1706
+ const separator = "\u2500".repeat(
1707
+ columnWidths.pathWidth + columnWidths.ownerWidth + columnWidths.shareWidth + columnWidths.linesWidth + 3
1708
+ );
1709
+ console.log(chalk4.dim(`\u2500\u2500\u2500\u252C${separator}`));
1710
+ }
1711
+ function formatDirectoryName(name, maxWidth) {
1712
+ const displayName = name.startsWith(".") ? name : name;
1713
+ const fullName = `\u{1F4C2} ${displayName}/`;
1714
+ const visibleLength = stripAnsi(fullName).length;
1715
+ if (visibleLength <= maxWidth) {
1716
+ return fullName;
1717
+ }
1718
+ const truncated = `${fullName.slice(0, maxWidth - 3)}...`;
1719
+ return truncated;
1720
+ }
1721
+ function printOwnershipTree(files, options = {}) {
1722
+ const { maxDepth = Infinity, directory } = options;
1723
+ const tree = buildDirectoryTree(files);
1724
+ calculateDirectoryStats(tree);
1725
+ const flattenedTree = flattenSingleChildPaths(tree);
1726
+ calculateDirectoryStats(flattenedTree);
1727
+ const terminalWidth = process.stdout.columns || 80;
1728
+ const columnWidths = calculateColumnWidths(terminalWidth);
1729
+ console.log(chalk4.bold.cyan("\u{1F4C1} Logic Distribution (Knowledge base)"));
1730
+ console.log(
1731
+ chalk4.dim(" Density: ") + chalk4.red.bold("\u2588 Solo (>80%)") + chalk4.dim(" | ") + chalk4.yellow.bold("\u2593 Focused (>50%)") + chalk4.dim(" | ") + chalk4.green.bold("\u2591 Distributed")
1732
+ );
1733
+ console.log("");
1734
+ printKnowledgeConcentration(flattenedTree, columnWidths, directory);
1735
+ printHeader(columnWidths);
1736
+ function printNode(node, prefix, isLast, depth, isRoot = false) {
1737
+ if (depth > maxDepth) {
1738
+ return;
1739
+ }
1740
+ if (isRoot) {
1741
+ let children = node.children.filter((child) => child.type === "directory");
1742
+ if (directory) {
1743
+ const normalizedDir = directory.replace(/^\.\//, "").replace(/\/$/, "");
1744
+ const dirParts = normalizedDir.split("/");
1745
+ const firstDirPart = dirParts[0];
1746
+ children = children.filter((child) => {
1747
+ const normalizedPath = child.path.replace(/^\.\//, "");
1748
+ return normalizedPath === firstDirPart || normalizedPath.startsWith(`${firstDirPart}/`);
1749
+ });
1750
+ }
1751
+ const processedChildren = [];
1752
+ let trivialGroup = [];
1753
+ for (let i = 0; i < children.length; i++) {
1754
+ const child = children[i];
1755
+ const isTrivial = shouldCollapseTrivialFolder(child);
1756
+ if (isTrivial) {
1757
+ trivialGroup.push(child);
1758
+ } else {
1759
+ if (trivialGroup.length > 0) {
1760
+ const trivialPrefix = "";
1761
+ console.log(
1762
+ trivialPrefix + chalk4.dim(
1763
+ `\u2514\u2500\u2500 [+ ${trivialGroup.length} minor folder${trivialGroup.length !== 1 ? "s" : ""}]`
1764
+ )
1765
+ );
1766
+ trivialGroup = [];
1767
+ }
1768
+ processedChildren.push(child);
1769
+ }
1770
+ }
1771
+ if (trivialGroup.length > 0) {
1772
+ console.log(
1773
+ chalk4.dim(
1774
+ `\u2514\u2500\u2500 [+ ${trivialGroup.length} minor folder${trivialGroup.length !== 1 ? "s" : ""}]`
1775
+ )
1776
+ );
1777
+ }
1778
+ for (let i = 0; i < processedChildren.length; i++) {
1779
+ const child = processedChildren[i];
1780
+ const isLastChild = i === processedChildren.length - 1;
1781
+ printNode(child, "", isLastChild, depth + 1, false);
1782
+ }
1783
+ return;
1784
+ }
1785
+ if (node.type === "directory") {
1786
+ if (directory) {
1787
+ const normalizedDir = directory.replace(/^\.\//, "").replace(/\/$/, "");
1788
+ const normalizedPath = node.path.replace(/^\.\//, "");
1789
+ if (normalizedPath !== normalizedDir && !normalizedPath.startsWith(`${normalizedDir}/`) && !normalizedDir.startsWith(`${normalizedPath}/`)) {
1790
+ return;
1791
+ }
1792
+ }
1793
+ const isTrivial = shouldCollapseTrivialFolder(node);
1794
+ if (!isTrivial) {
1795
+ if (node.owner && node.share !== void 0 && node.lines !== void 0) {
1796
+ const densityIndicator = getDensityIndicator(node.share);
1797
+ const displayName = formatDirectoryName(
1798
+ node.name,
1799
+ columnWidths.pathWidth - prefix.length - 4
1800
+ );
1801
+ const pathDisplay = padRight(
1802
+ prefix + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ") + displayName,
1803
+ columnWidths.pathWidth
1804
+ );
1805
+ let ownerDisplay = padRight(chalk4.bold(node.owner), columnWidths.ownerWidth);
1806
+ let shareDisplay = padLeft(`${node.share}%`, columnWidths.shareWidth);
1807
+ let linesDisplay = padLeft(formatNumber(node.lines), columnWidths.linesWidth);
1808
+ let dateDisplay = "";
1809
+ if (node.lastCommitDate) {
1810
+ const relativeTime = formatRelativeTime(node.lastCommitDate);
1811
+ dateDisplay = ` / ${relativeTime}`;
1812
+ }
1813
+ if (directory && isTestOrSnapshotPath(node.path)) {
1814
+ ownerDisplay = padRight(chalk4.gray(node.owner), columnWidths.ownerWidth);
1815
+ shareDisplay = padLeft(chalk4.gray(`${node.share}%`), columnWidths.shareWidth);
1816
+ linesDisplay = padLeft(chalk4.gray(formatNumber(node.lines)), columnWidths.linesWidth);
1817
+ const grayPathDisplay = padRight(
1818
+ chalk4.gray(prefix + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ") + displayName),
1819
+ columnWidths.pathWidth
1820
+ );
1821
+ console.log(
1822
+ `${grayPathDisplay} ${ownerDisplay} ${shareDisplay} ${linesDisplay}${chalk4.gray(dateDisplay)}`
1823
+ );
1824
+ } else {
1825
+ let coloredIndicator = densityIndicator;
1826
+ if (node.share > 80) {
1827
+ coloredIndicator = chalk4.red.bold(densityIndicator);
1828
+ } else if (node.share > 50) {
1829
+ coloredIndicator = chalk4.yellow.bold(densityIndicator);
1830
+ } else {
1831
+ coloredIndicator = chalk4.green.bold(densityIndicator);
1832
+ }
1833
+ console.log(
1834
+ `${pathDisplay} ${coloredIndicator} ${ownerDisplay} ${shareDisplay} ${linesDisplay}${chalk4.dim(dateDisplay)}`
1835
+ );
1836
+ }
1837
+ }
1838
+ if (node.children.length > 0) {
1839
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
1840
+ let visibleChildren = node.children.filter((child) => child.type === "directory");
1841
+ if (directory) {
1842
+ const normalizedDir = directory.replace(/^\.\//, "").replace(/\/$/, "");
1843
+ visibleChildren = visibleChildren.filter((child) => {
1844
+ const normalizedPath = child.path.replace(/^\.\//, "");
1845
+ return normalizedPath === normalizedDir || normalizedPath.startsWith(`${normalizedDir}/`) || normalizedDir.startsWith(`${normalizedPath}/`);
1846
+ });
1847
+ }
1848
+ const processedChildren = [];
1849
+ let trivialGroup = [];
1850
+ for (let i = 0; i < visibleChildren.length; i++) {
1851
+ const child = visibleChildren[i];
1852
+ const isTrivialChild = shouldCollapseTrivialFolder(child);
1853
+ if (isTrivialChild) {
1854
+ trivialGroup.push(child);
1855
+ } else {
1856
+ if (trivialGroup.length > 0) {
1857
+ console.log(
1858
+ childPrefix + chalk4.dim(
1859
+ `\u2514\u2500\u2500 [+ ${trivialGroup.length} minor folder${trivialGroup.length !== 1 ? "s" : ""}]`
1860
+ )
1861
+ );
1862
+ trivialGroup = [];
1863
+ }
1864
+ processedChildren.push(child);
1865
+ }
1866
+ }
1867
+ if (trivialGroup.length > 0) {
1868
+ console.log(
1869
+ childPrefix + chalk4.dim(
1870
+ `\u2514\u2500\u2500 [+ ${trivialGroup.length} minor folder${trivialGroup.length !== 1 ? "s" : ""}]`
1871
+ )
1872
+ );
1873
+ }
1874
+ for (let i = 0; i < processedChildren.length; i++) {
1875
+ const child = processedChildren[i];
1876
+ const isLastChild = i === processedChildren.length - 1;
1877
+ printNode(child, childPrefix, isLastChild, depth + 1, false);
1878
+ }
1879
+ if (node.children.some((child) => child.type === "file")) {
1880
+ const fileCount = node.children.filter((child) => child.type === "file").length;
1881
+ console.log(
1882
+ childPrefix + chalk4.dim(`\u2514\u2500\u2500 ... ${fileCount} file${fileCount !== 1 ? "s" : ""}`)
1883
+ );
1884
+ }
1885
+ }
1886
+ }
1887
+ }
1888
+ }
1889
+ printNode(flattenedTree, "", true, 0, true);
1890
+ console.log("");
1891
+ }
1892
+
1893
+ // src/cli/output/printers/ownership-printer.ts
1894
+ var OwnershipPrinter = class {
1895
+ directory;
1896
+ constructor(directory) {
1897
+ this.directory = directory;
1898
+ }
1899
+ print(result, _timeRangeDescription) {
1900
+ this.printFileOwnershipTree(result.files);
1901
+ }
1902
+ printFileOwnershipTree(files) {
1903
+ const fileEntries = Object.values(files);
1904
+ if (fileEntries.length === 0) {
1905
+ console.log(chalk5.yellow(`
1906
+ \u26A0 No files found in the specified time range.
1907
+ `));
1908
+ return;
1909
+ }
1910
+ const filesRecord = {};
1911
+ for (const file of fileEntries) {
1912
+ filesRecord[file.file] = file;
1913
+ }
1914
+ printOwnershipTree(filesRecord, {
1915
+ maxDepth: 10,
1916
+ directory: this.directory
1917
+ });
1918
+ }
1919
+ };
1920
+
1921
+ // src/cli/output/printers/period-printer.ts
1922
+ import chalk6 from "chalk";
1923
+ var PeriodPrinter = class {
1924
+ constructor(periodUnit) {
1925
+ this.periodUnit = periodUnit;
1926
+ }
1927
+ print(result, timeRangeDescription) {
1928
+ if (result.length === 0) {
1929
+ console.log(chalk6.yellow(`
1930
+ \u26A0 No commits found in the specified time range.
1931
+ `));
1932
+ return;
1933
+ }
1934
+ const periodLabel = this.periodUnit === "daily" ? "Date" : this.periodUnit === "weekly" ? "Week" : "Month";
1935
+ const table = createTable({
1936
+ head: [
1937
+ chalk6.bold(periodLabel),
1938
+ chalk6.bold("Author"),
1939
+ chalk6.bold("Commits"),
1940
+ chalk6.bold("Insertions"),
1941
+ chalk6.bold("Deletions"),
1942
+ chalk6.bold("Total Impact")
1943
+ ],
1944
+ colAligns: ["left", "left", "right", "right", "right", "right"]
1945
+ });
1946
+ const uniqueAuthors = new Set(result.map((s) => s.author)).size;
1947
+ const uniquePeriods = new Set(result.map((s) => s.period)).size;
1948
+ const totalCommits = result.reduce((sum, s) => sum + s.stats.commits, 0);
1949
+ const totalInsertions = result.reduce((sum, s) => sum + s.stats.insertions, 0);
1950
+ const totalDeletions = result.reduce((sum, s) => sum + s.stats.deletions, 0);
1951
+ for (const item of result) {
1952
+ const totalImpact = item.stats.insertions + item.stats.deletions;
1953
+ table.push([
1954
+ chalk6.cyan(item.period),
1955
+ item.author,
1956
+ chalk6.white(item.stats.commits.toString()),
1957
+ chalk6.green(`+${item.stats.insertions.toLocaleString()}`),
1958
+ chalk6.red(`-${item.stats.deletions.toLocaleString()}`),
1959
+ chalk6.bold(totalImpact.toLocaleString())
1960
+ ]);
1961
+ }
1962
+ table.push(
1963
+ createSummaryRow({
1964
+ totalCommits,
1965
+ totalInsertions,
1966
+ totalDeletions,
1967
+ timeRangeDescription,
1968
+ additionalInfo: `${uniqueAuthors} authors / ${uniquePeriods} ${periodLabel.toLowerCase()}s`
1969
+ })
1970
+ );
1971
+ console.log(`
1972
+ ${table.toString()}
1973
+ `);
1974
+ }
1975
+ };
1976
+
1977
+ // src/cli/output/printers/printer-factory.ts
1978
+ function createPrinter(mode, periodUnit = "daily", directory) {
1979
+ switch (mode) {
1980
+ case "aggregate":
1981
+ return new AggregatePrinter();
1982
+ case "periodic":
1983
+ return new PeriodPrinter(periodUnit);
1984
+ case "ownership":
1985
+ return new OwnershipPrinter(directory);
1986
+ default:
1987
+ throw new Error(`Unknown analysis mode: ${mode}`);
1988
+ }
1989
+ }
1990
+
1991
+ // src/cli/validators/cli-options-validator.ts
1992
+ function validateCLIOptions(options) {
1993
+ if (!VALID_MODES.includes(options.mode)) {
1994
+ return {
1995
+ success: false,
1996
+ error: `Invalid --mode value: ${options.mode}. Must be one of: ${VALID_MODES.join(", ")}`
1997
+ };
1998
+ }
1999
+ if (options.periodUnit && !VALID_PERIOD_UNITS.includes(options.periodUnit)) {
2000
+ return {
2001
+ success: false,
2002
+ error: `Invalid --period-unit value: ${options.periodUnit}. Must be one of: ${VALID_PERIOD_UNITS.join(", ")}`
2003
+ };
2004
+ }
2005
+ if (options.since || options.until) {
2006
+ try {
2007
+ validateTimeRange(options.since, options.until);
2008
+ } catch (error) {
2009
+ return {
2010
+ success: false,
2011
+ error: error instanceof Error ? error.message : "Invalid time range"
2012
+ };
2013
+ }
2014
+ }
2015
+ if (options.days !== void 0 && options.days <= 0) {
2016
+ return {
2017
+ success: false,
2018
+ error: "--days must be a positive number"
2019
+ };
2020
+ }
2021
+ return { success: true };
2022
+ }
2023
+
2024
+ // src/cli/index.ts
2025
+ async function main() {
2026
+ const program = createCommand();
2027
+ const rawOptions = parseArgs(program);
2028
+ const options = {
2029
+ ...rawOptions,
2030
+ mode: rawOptions.mode || "aggregate"
2031
+ };
2032
+ const validationResult = validateCLIOptions(options);
2033
+ if (!validationResult.success) {
2034
+ console.error(chalk7.red(validationResult.error));
2035
+ process.exit(1);
2036
+ }
2037
+ const analyzerOptions = adaptToAnalyzerOptions(options);
2038
+ const timeRangeDescription = buildTimeRangeDescription(options);
2039
+ p.intro(chalk7.bgCyan(chalk7.black(" gimpact ")));
2040
+ const s = p.spinner();
2041
+ s.start("Analyzing Git repository...");
2042
+ try {
2043
+ const stats = await analyzeContributions(analyzerOptions);
2044
+ s.stop("Analysis complete!");
2045
+ const printer = createPrinter(
2046
+ analyzerOptions.mode || "aggregate",
2047
+ analyzerOptions.periodUnit,
2048
+ analyzerOptions.directory
2049
+ );
2050
+ printer.print(stats, timeRangeDescription);
2051
+ p.outro(chalk7.green("\u2713 Done!"));
2052
+ } catch (error) {
2053
+ s.stop("Analysis failed");
2054
+ p.cancel(chalk7.red(error instanceof Error ? error.message : "Unknown error occurred"));
2055
+ process.exit(1);
2056
+ }
2057
+ }
2058
+ main().catch((error) => {
2059
+ console.error(chalk7.red("Fatal error:"), error);
2060
+ process.exit(1);
2061
+ });