@cod3vil/kount-cli 1.0.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.
@@ -0,0 +1,95 @@
1
+ import type { AnalyzedFileData, AnalyzerPlugin, PluginResult } from '../types.js';
2
+
3
+ /**
4
+ * Known extension-to-language name mappings.
5
+ */
6
+ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
7
+ '.js': 'JavaScript',
8
+ '.jsx': 'JavaScript (JSX)',
9
+ '.ts': 'TypeScript',
10
+ '.tsx': 'TypeScript (TSX)',
11
+ '.py': 'Python',
12
+ '.rb': 'Ruby',
13
+ '.java': 'Java',
14
+ '.c': 'C',
15
+ '.cpp': 'C++',
16
+ '.cs': 'C#',
17
+ '.go': 'Go',
18
+ '.rs': 'Rust',
19
+ '.swift': 'Swift',
20
+ '.kt': 'Kotlin',
21
+ '.dart': 'Dart',
22
+ '.scala': 'Scala',
23
+ '.php': 'PHP',
24
+ '.html': 'HTML',
25
+ '.htm': 'HTML',
26
+ '.css': 'CSS',
27
+ '.scss': 'SCSS',
28
+ '.less': 'LESS',
29
+ '.xml': 'XML',
30
+ '.svg': 'SVG',
31
+ '.json': 'JSON',
32
+ '.jsonc': 'JSONC',
33
+ '.yaml': 'YAML',
34
+ '.yml': 'YAML',
35
+ '.md': 'Markdown',
36
+ '.sh': 'Shell',
37
+ '.bash': 'Shell',
38
+ '.zsh': 'Shell',
39
+ '.sql': 'SQL',
40
+ '.lua': 'Lua',
41
+ '.r': 'R',
42
+ '.m': 'Objective-C',
43
+ '.mm': 'Objective-C++',
44
+ '.pl': 'Perl',
45
+ '.pm': 'Perl',
46
+ '.hs': 'Haskell',
47
+ '.lisp': 'Lisp',
48
+ '.clj': 'Clojure',
49
+ '.scm': 'Scheme',
50
+ '.vue': 'Vue',
51
+ '.toml': 'TOML',
52
+ '.ini': 'INI',
53
+ '.cfg': 'Config',
54
+ '.ps1': 'PowerShell',
55
+ '.ada': 'Ada',
56
+ '.vhdl': 'VHDL',
57
+ };
58
+
59
+ /**
60
+ * Computes language distribution across all scanned files.
61
+ * The perFile map stores a value of 1 for each file (used for counting),
62
+ * while summaryValue is the number of distinct languages found.
63
+ */
64
+ export class LanguageDistributionPlugin implements AnalyzerPlugin {
65
+ readonly name = 'LanguageDistribution';
66
+
67
+ analyze(files: AnalyzedFileData[]): PluginResult {
68
+ const langCounts = new Map<string, number>();
69
+ const perFile = new Map<string, number>();
70
+
71
+ for (const file of files) {
72
+ const ext = file.extension.toLowerCase();
73
+ const language = EXTENSION_TO_LANGUAGE[ext] || 'Other';
74
+ langCounts.set(language, (langCounts.get(language) || 0) + 1);
75
+ perFile.set(file.filePath, 1);
76
+ }
77
+
78
+ // summaryValue = number of distinct languages
79
+ return { pluginName: this.name, summaryValue: langCounts.size, perFile };
80
+ }
81
+
82
+ /**
83
+ * Helper to retrieve the full language distribution map.
84
+ * Called by the aggregator after analyze().
85
+ */
86
+ getDistribution(files: AnalyzedFileData[]): Map<string, number> {
87
+ const langCounts = new Map<string, number>();
88
+ for (const file of files) {
89
+ const ext = file.extension.toLowerCase();
90
+ const language = EXTENSION_TO_LANGUAGE[ext] || 'Other';
91
+ langCounts.set(language, (langCounts.get(language) || 0) + 1);
92
+ }
93
+ return langCounts;
94
+ }
95
+ }
@@ -0,0 +1,41 @@
1
+ import type { AnalyzedFileData, AnalyzerPlugin, PluginResult } from '../types.js';
2
+
3
+ const DEFAULT_TOP_N = 10;
4
+
5
+ /**
6
+ * Identifies the largest files by byte size.
7
+ * summaryValue = the size of the single largest file.
8
+ * perFile = the size of each file (same as FileSize, but sorted/limited).
9
+ */
10
+ export class LargestFilesPlugin implements AnalyzerPlugin {
11
+ readonly name = 'LargestFiles';
12
+ private topN: number;
13
+
14
+ constructor(topN: number = DEFAULT_TOP_N) {
15
+ this.topN = topN;
16
+ }
17
+
18
+ analyze(files: AnalyzedFileData[]): PluginResult {
19
+ const sorted = [...files].sort((a, b) => b.size - a.size);
20
+ const top = sorted.slice(0, this.topN);
21
+
22
+ const perFile = new Map<string, number>();
23
+ for (const file of top) {
24
+ perFile.set(file.filePath, file.size);
25
+ }
26
+
27
+ const summaryValue = top.length > 0 ? top[0].size : 0;
28
+
29
+ return { pluginName: this.name, summaryValue, perFile };
30
+ }
31
+
32
+ /**
33
+ * Helper to get the ranked list, consumed by reporters.
34
+ */
35
+ getTopFiles(files: AnalyzedFileData[]): Array<{ filePath: string; size: number }> {
36
+ return [...files]
37
+ .sort((a, b) => b.size - a.size)
38
+ .slice(0, this.topN)
39
+ .map(f => ({ filePath: f.filePath, size: f.size }));
40
+ }
41
+ }
@@ -0,0 +1,18 @@
1
+ import type { AnalyzedFileData, AnalyzerPlugin, PluginResult } from '../types.js';
2
+
3
+ /**
4
+ * Counts total number of files scanned.
5
+ */
6
+ export class TotalFilesPlugin implements AnalyzerPlugin {
7
+ readonly name = 'TotalFiles';
8
+
9
+ analyze(files: AnalyzedFileData[]): PluginResult {
10
+ const perFile = new Map<string, number>();
11
+
12
+ for (const file of files) {
13
+ perFile.set(file.filePath, 1);
14
+ }
15
+
16
+ return { pluginName: this.name, summaryValue: files.length, perFile };
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ import type { AnalyzedFileData, AnalyzerPlugin, PluginResult } from '../types.js';
2
+
3
+ /**
4
+ * Counts total lines across all scanned files.
5
+ */
6
+ export class TotalLinesPlugin implements AnalyzerPlugin {
7
+ readonly name = 'TotalLines';
8
+
9
+ analyze(files: AnalyzedFileData[]): PluginResult {
10
+ const perFile = new Map<string, number>();
11
+ let summaryValue = 0;
12
+
13
+ for (const file of files) {
14
+ const lineCount = file.lines.length;
15
+ perFile.set(file.filePath, lineCount);
16
+ summaryValue += lineCount;
17
+ }
18
+
19
+ return { pluginName: this.name, summaryValue, perFile };
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ export { BlankLinesPlugin } from './built-in/blank-lines.js';
2
+ export { CommentLinesPlugin } from './built-in/comment-lines.js';
3
+ export { FileSizePlugin } from './built-in/file-size.js';
4
+ export { LanguageDistributionPlugin } from './built-in/language-distribution.js';
5
+ export { LargestFilesPlugin } from './built-in/largest-files.js';
6
+ export { TotalFilesPlugin } from './built-in/total-files.js';
7
+ export { TotalLinesPlugin } from './built-in/total-lines.js';
8
+
9
+ export type { AnalyzedFileData, AnalyzerPlugin, PluginResult, ProjectStats } from './types.js';
10
+
@@ -0,0 +1,58 @@
1
+
2
+ /**
3
+ * Represents the result of a single plugin's analysis on the entire project.
4
+ */
5
+ export interface PluginResult {
6
+ /** The name of the plugin that produced this result. */
7
+ pluginName: string;
8
+ /** Summary-level metric value (e.g. total lines across all files). */
9
+ summaryValue: number;
10
+ /** Per-file breakdown of the metric, keyed by file path. */
11
+ perFile: Map<string, number>;
12
+ }
13
+
14
+ /**
15
+ * The contract every analyzer plugin must implement.
16
+ * Plugins receive the full list of scanned files and their line data,
17
+ * then return aggregated results.
18
+ */
19
+ export interface AnalyzerPlugin {
20
+ /** Unique name of this plugin (e.g. 'TotalLines'). */
21
+ name: string;
22
+ /** Analyze scanned files and return aggregated results. */
23
+ analyze(files: AnalyzedFileData[]): PluginResult;
24
+ }
25
+
26
+ /**
27
+ * Enriched file data created by the aggregator after streaming each file.
28
+ * Contains raw line data so plugins don't need to re-read files.
29
+ */
30
+ export interface AnalyzedFileData {
31
+ /** Absolute path to the scanned file. */
32
+ filePath: string;
33
+ /** File size in bytes (from stat). */
34
+ size: number;
35
+ /** File extension including the dot, e.g. '.ts'. */
36
+ extension: string;
37
+ /** All lines of the file as strings. */
38
+ lines: string[];
39
+ }
40
+
41
+ /**
42
+ * The final aggregated stats payload produced by the Orchestrator.
43
+ * Consumed by reporters to render output.
44
+ */
45
+ export interface ProjectStats {
46
+ /** Root directory that was scanned. */
47
+ rootDir: string;
48
+ /** Total number of files scanned. */
49
+ totalFiles: number;
50
+ /** Results from each plugin, keyed by plugin name. */
51
+ pluginResults: Map<string, PluginResult>;
52
+ /** Language distribution: language name -> file count. */
53
+ languageDistribution: Map<string, number>;
54
+ /** Top N largest files by size. */
55
+ largestFiles: Array<{ filePath: string; size: number }>;
56
+ /** Timestamp of when the scan completed. */
57
+ scannedAt: Date;
58
+ }
File without changes
@@ -0,0 +1,385 @@
1
+ import { exec } from 'node:child_process';
2
+ import http from 'node:http';
3
+ import path from 'node:path';
4
+ import type { ProjectStats } from '../plugins/types.js';
5
+
6
+ /**
7
+ * Generates the full HTML dashboard page with injected data.
8
+ * Uses Tailwind CSS (CDN) and Alpine.js (CDN) for styling and interactivity.
9
+ */
10
+ function generateHtmlDashboard(stats: ProjectStats): string {
11
+ const totalLines = stats.pluginResults.get('TotalLines')?.summaryValue ?? 0;
12
+ const blankLines = stats.pluginResults.get('BlankLines')?.summaryValue ?? 0;
13
+ const commentLines = stats.pluginResults.get('CommentLines')?.summaryValue ?? 0;
14
+ const totalBytes = stats.pluginResults.get('FileSize')?.summaryValue ?? 0;
15
+ const codeLines = totalLines - blankLines - commentLines;
16
+ const codeRatio = totalLines > 0 ? ((codeLines / totalLines) * 100).toFixed(1) : '0.0';
17
+
18
+ const formatSize = (bytes: number): string => {
19
+ if (bytes < 1024) return `${bytes} B`;
20
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
21
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
22
+ };
23
+
24
+ // Prepare language data for Alpine.js
25
+ const langData = [...stats.languageDistribution.entries()]
26
+ .sort((a, b) => b[1] - a[1])
27
+ .map(([lang, count]) => ({
28
+ lang,
29
+ count,
30
+ pct: ((count / stats.totalFiles) * 100).toFixed(1),
31
+ }));
32
+
33
+ // Prepare largest files data
34
+ const largestData = stats.largestFiles.map((f, i) => ({
35
+ rank: i + 1,
36
+ path: path.relative(stats.rootDir, f.filePath),
37
+ size: formatSize(f.size),
38
+ rawSize: f.size,
39
+ }));
40
+
41
+ const jsonData = JSON.stringify({
42
+ summary: {
43
+ files: stats.totalFiles,
44
+ totalLines,
45
+ codeLines,
46
+ commentLines,
47
+ blankLines,
48
+ codeRatio,
49
+ totalSize: formatSize(totalBytes),
50
+ },
51
+ languages: langData,
52
+ largestFiles: largestData,
53
+ scannedAt: stats.scannedAt.toISOString(),
54
+ rootDir: stats.rootDir,
55
+ });
56
+
57
+ return `<!DOCTYPE html>
58
+ <html lang="en">
59
+ <head>
60
+ <meta charset="UTF-8">
61
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
62
+ <title>KOUNT Dashboard</title>
63
+ <script src="https://cdn.tailwindcss.com"></script>
64
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
65
+ <link rel="preconnect" href="https://fonts.googleapis.com">
66
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
67
+ <style>
68
+ body { font-family: 'Inter', system-ui, sans-serif; }
69
+ [x-cloak] { display: none !important; }
70
+ </style>
71
+ </head>
72
+ <body class="bg-gray-950 text-white min-h-screen">
73
+ <div x-data="dashboard()" x-cloak class="max-w-6xl mx-auto px-6 py-8">
74
+
75
+ <!-- Header with Tab Navigation -->
76
+ <div class="mb-8">
77
+ <div class="flex items-center justify-between">
78
+ <div>
79
+ <h1 class="text-3xl font-bold text-cyan-400 tracking-tight">KOUNT</h1>
80
+ <p class="text-gray-400 mt-1">Project Intelligence for <span class="text-white font-mono" x-text="data.rootDir"></span></p>
81
+ </div>
82
+ <nav class="flex gap-1 bg-gray-900 rounded-lg p-1 border border-gray-800">
83
+ <button
84
+ @click="currentTab = 'dashboard'"
85
+ :class="currentTab === 'dashboard' ? 'bg-cyan-500/20 text-cyan-400' : 'text-gray-500 hover:text-gray-300'"
86
+ class="px-4 py-2 rounded-md text-sm font-medium transition-all duration-200"
87
+ >Dashboard</button>
88
+ <button
89
+ @click="currentTab = 'help'"
90
+ :class="currentTab === 'help' ? 'bg-cyan-500/20 text-cyan-400' : 'text-gray-500 hover:text-gray-300'"
91
+ class="px-4 py-2 rounded-md text-sm font-medium transition-all duration-200"
92
+ >Help</button>
93
+ </nav>
94
+ </div>
95
+ <p class="text-gray-600 text-sm mt-1" x-show="currentTab === 'dashboard'">Scanned <span x-text="new Date(data.scannedAt).toLocaleString()"></span></p>
96
+ </div>
97
+
98
+ <!-- ==================== DASHBOARD TAB ==================== -->
99
+ <div x-show="currentTab === 'dashboard'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-1" x-transition:enter-end="opacity-100 translate-y-0">
100
+
101
+ <!-- Summary Cards -->
102
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
103
+ <template x-for="card in summaryCards" :key="card.label">
104
+ <div class="bg-gray-900 rounded-xl p-4 border border-gray-800">
105
+ <p class="text-gray-400 text-sm" x-text="card.label"></p>
106
+ <p class="text-2xl font-bold mt-1" :class="card.color" x-text="card.value"></p>
107
+ </div>
108
+ </template>
109
+ </div>
110
+
111
+ <!-- Language Distribution -->
112
+ <div class="bg-gray-900 rounded-xl p-6 border border-gray-800 mb-8" x-show="data.languages.length > 0">
113
+ <div class="flex items-center justify-between mb-4">
114
+ <h2 class="text-lg font-semibold text-blue-400">Language Distribution</h2>
115
+ <button
116
+ @click="langSort = langSort === 'count' ? 'name' : 'count'"
117
+ class="text-sm text-gray-500 hover:text-white transition-colors px-3 py-1 rounded bg-gray-800"
118
+ x-text="'Sort by ' + (langSort === 'count' ? 'Name' : 'Count')"
119
+ ></button>
120
+ </div>
121
+ <div class="space-y-2">
122
+ <template x-for="lang in sortedLanguages" :key="lang.lang">
123
+ <div class="flex items-center gap-3">
124
+ <span class="w-28 text-sm text-gray-300 truncate" x-text="lang.lang"></span>
125
+ <div class="flex-1 bg-gray-800 rounded-full h-5 overflow-hidden">
126
+ <div class="h-full bg-gradient-to-r from-cyan-600 to-blue-500 rounded-full transition-all duration-300"
127
+ :style="'width:' + lang.pct + '%'"></div>
128
+ </div>
129
+ <span class="text-sm text-gray-400 w-20 text-right" x-text="lang.count + ' (' + lang.pct + '%)'"></span>
130
+ </div>
131
+ </template>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Largest Files Table -->
136
+ <div class="bg-gray-900 rounded-xl p-6 border border-gray-800" x-show="data.largestFiles.length > 0">
137
+ <h2 class="text-lg font-semibold text-purple-400 mb-4">Top Largest Files</h2>
138
+ <table class="w-full text-sm">
139
+ <thead>
140
+ <tr class="text-gray-500 border-b border-gray-800">
141
+ <th class="text-left py-2 w-12">#</th>
142
+ <th class="text-left py-2">File</th>
143
+ <th class="text-right py-2 w-24">Size</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ <template x-for="file in data.largestFiles" :key="file.rank">
148
+ <tr class="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors">
149
+ <td class="py-2 text-gray-500" x-text="file.rank"></td>
150
+ <td class="py-2 font-mono text-gray-300 truncate max-w-md" x-text="file.path"></td>
151
+ <td class="py-2 text-right text-yellow-400" x-text="file.size"></td>
152
+ </tr>
153
+ </template>
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+
158
+ </div>
159
+
160
+ <!-- ==================== HELP TAB ==================== -->
161
+ <div x-show="currentTab === 'help'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-1" x-transition:enter-end="opacity-100 translate-y-0">
162
+
163
+ <!-- About Kount -->
164
+ <div class="bg-gray-900 rounded-xl p-6 border border-gray-800 mb-6">
165
+ <h2 class="text-lg font-semibold text-cyan-400 mb-3">About KOUNT</h2>
166
+ <p class="text-gray-300 leading-relaxed mb-4">
167
+ KOUNT is a codebase intelligence terminal tool that analyzes your projects with precision.
168
+ It streams files efficiently, respects <code class="text-cyan-300 bg-gray-800 px-1.5 py-0.5 rounded text-xs">.gitignore</code> rules,
169
+ caches results for speed, and outputs beautiful reports in your terminal, as Markdown, or as an interactive HTML dashboard.
170
+ </p>
171
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
172
+ <div class="bg-gray-800/50 rounded-lg p-3 border border-gray-700/50">
173
+ <p class="text-cyan-400 font-semibold text-sm">Stream-Based</p>
174
+ <p class="text-gray-400 text-xs mt-1">Files are read chunk-by-chunk, never fully loaded into memory.</p>
175
+ </div>
176
+ <div class="bg-gray-800/50 rounded-lg p-3 border border-gray-700/50">
177
+ <p class="text-green-400 font-semibold text-sm">Incremental Cache</p>
178
+ <p class="text-gray-400 text-xs mt-1">Uses mtime + size invalidation to skip unchanged files on re-runs.</p>
179
+ </div>
180
+ <div class="bg-gray-800/50 rounded-lg p-3 border border-gray-700/50">
181
+ <p class="text-purple-400 font-semibold text-sm">Plugin Architecture</p>
182
+ <p class="text-gray-400 text-xs mt-1">7 built-in analyzers: lines, blanks, comments, size, files, languages, largest.</p>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- CLI Commands & Flags -->
188
+ <div class="bg-gray-900 rounded-xl p-6 border border-gray-800 mb-6">
189
+ <h2 class="text-lg font-semibold text-blue-400 mb-4">CLI Commands &amp; Flags</h2>
190
+ <div class="bg-gray-800/50 rounded-lg p-4 mb-4 font-mono text-sm">
191
+ <p class="text-gray-400 mb-1"># Basic usage</p>
192
+ <p class="text-green-400">$ kount</p>
193
+ <p class="text-gray-400 mt-3 mb-1"># Scan a specific directory and output Markdown</p>
194
+ <p class="text-green-400">$ kount --root-dir ./my-project --output-mode markdown</p>
195
+ <p class="text-gray-400 mt-3 mb-1"># Open an HTML dashboard</p>
196
+ <p class="text-green-400">$ kount -o html</p>
197
+ </div>
198
+ <table class="w-full text-sm">
199
+ <thead>
200
+ <tr class="text-gray-500 border-b border-gray-800">
201
+ <th class="text-left py-2">Flag</th>
202
+ <th class="text-left py-2">Alias</th>
203
+ <th class="text-left py-2">Description</th>
204
+ </tr>
205
+ </thead>
206
+ <tbody class="text-gray-300">
207
+ <tr class="border-b border-gray-800/50">
208
+ <td class="py-2 font-mono text-cyan-300">--root-dir &lt;path&gt;</td>
209
+ <td class="py-2 font-mono text-gray-500">-d</td>
210
+ <td class="py-2">Root directory to scan (default: current directory)</td>
211
+ </tr>
212
+ <tr class="border-b border-gray-800/50">
213
+ <td class="py-2 font-mono text-cyan-300">--output-mode &lt;mode&gt;</td>
214
+ <td class="py-2 font-mono text-gray-500">-o</td>
215
+ <td class="py-2">Output mode: <span class="text-white">terminal</span>, <span class="text-white">markdown</span>, or <span class="text-white">html</span></td>
216
+ </tr>
217
+ <tr class="border-b border-gray-800/50">
218
+ <td class="py-2 font-mono text-cyan-300">--include-tests</td>
219
+ <td class="py-2 font-mono text-gray-500">-t</td>
220
+ <td class="py-2">Include test files in the analysis</td>
221
+ </tr>
222
+ <tr class="border-b border-gray-800/50">
223
+ <td class="py-2 font-mono text-cyan-300">--force</td>
224
+ <td class="py-2 font-mono text-gray-500">-f</td>
225
+ <td class="py-2">Force overwrite output files (markdown mode)</td>
226
+ </tr>
227
+ <tr class="border-b border-gray-800/50">
228
+ <td class="py-2 font-mono text-cyan-300">--output &lt;path&gt;</td>
229
+ <td class="py-2 font-mono text-gray-500"></td>
230
+ <td class="py-2">Specify output file path (markdown mode)</td>
231
+ </tr>
232
+ <tr class="border-b border-gray-800/50">
233
+ <td class="py-2 font-mono text-cyan-300">--no-gitignore</td>
234
+ <td class="py-2 font-mono text-gray-500"></td>
235
+ <td class="py-2">Ignore .gitignore rules during scanning</td>
236
+ </tr>
237
+ <tr class="border-b border-gray-800/50">
238
+ <td class="py-2 font-mono text-cyan-300">--no-cache</td>
239
+ <td class="py-2 font-mono text-gray-500"></td>
240
+ <td class="py-2">Disable caching for this run</td>
241
+ </tr>
242
+ <tr class="border-b border-gray-800/50">
243
+ <td class="py-2 font-mono text-cyan-300">--clear-cache</td>
244
+ <td class="py-2 font-mono text-gray-500"></td>
245
+ <td class="py-2">Clear the cache before scanning</td>
246
+ </tr>
247
+ <tr>
248
+ <td class="py-2 font-mono text-cyan-300">--version</td>
249
+ <td class="py-2 font-mono text-gray-500">-V</td>
250
+ <td class="py-2">Display version number</td>
251
+ </tr>
252
+ </tbody>
253
+ </table>
254
+ </div>
255
+
256
+ <!-- Developer Info -->
257
+ <div class="bg-gray-900 rounded-xl p-6 border border-gray-800">
258
+ <h2 class="text-lg font-semibold text-purple-400 mb-4">Developer</h2>
259
+ <div class="flex flex-col md:flex-row gap-6">
260
+ <div class="flex-1">
261
+ <h3 class="text-xl font-bold text-white mb-2">Michael Nji</h3>
262
+ <p class="text-gray-400 text-sm leading-relaxed mb-4">
263
+ Full stack web developer with a passion for building beautiful and robust web projects.
264
+ Coding since 2022, with 3+ years of experience building with modern web technologies.
265
+ Active open source contributor &mdash; including contributions to
266
+ <a href="https://github.com/biomejs/biome" target="_blank" class="text-cyan-400 hover:text-cyan-300 underline underline-offset-2">Biome</a> and
267
+ <a href="https://github.com/stepci/stepci" target="_blank" class="text-cyan-400 hover:text-cyan-300 underline underline-offset-2">StepCI</a>.
268
+ </p>
269
+ <div class="flex flex-wrap gap-3">
270
+ <a href="https://michaelnji.codes" target="_blank"
271
+ class="inline-flex items-center gap-2 px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 rounded-lg text-cyan-400 text-sm hover:bg-cyan-500/20 transition-all duration-200">
272
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>
273
+ Portfolio
274
+ </a>
275
+ <a href="https://github.com/michaelnji" target="_blank"
276
+ class="inline-flex items-center gap-2 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-300 text-sm hover:bg-gray-700 transition-all duration-200">
277
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
278
+ GitHub
279
+ </a>
280
+ <a href="https://michaelnji.codes/blog" target="_blank"
281
+ class="inline-flex items-center gap-2 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-300 text-sm hover:bg-gray-700 transition-all duration-200">
282
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/></svg>
283
+ Blog
284
+ </a>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ </div>
291
+
292
+ <!-- Footer -->
293
+ <div class="mt-8 text-center text-gray-700 text-sm">
294
+ Generated by KOUNT &mdash; Project Intelligence for Codebases
295
+ </div>
296
+
297
+ </div>
298
+
299
+ <script>
300
+ function dashboard() {
301
+ const data = ${jsonData};
302
+ return {
303
+ data,
304
+ currentTab: 'dashboard',
305
+ langSort: 'count',
306
+ get summaryCards() {
307
+ const s = this.data.summary;
308
+ return [
309
+ { label: 'Files', value: s.files.toLocaleString(), color: 'text-cyan-400' },
310
+ { label: 'Total Lines', value: s.totalLines.toLocaleString(), color: 'text-white' },
311
+ { label: 'Code Lines', value: s.codeLines.toLocaleString(), color: 'text-green-400' },
312
+ { label: 'Code Ratio', value: s.codeRatio + '%', color: 'text-green-400' },
313
+ { label: 'Comment Lines', value: s.commentLines.toLocaleString(), color: 'text-yellow-400' },
314
+ { label: 'Blank Lines', value: s.blankLines.toLocaleString(), color: 'text-gray-400' },
315
+ { label: 'Total Size', value: s.totalSize, color: 'text-cyan-400' },
316
+ ];
317
+ },
318
+ get sortedLanguages() {
319
+ const langs = [...this.data.languages];
320
+ if (this.langSort === 'name') {
321
+ langs.sort((a, b) => a.lang.localeCompare(b.lang));
322
+ } else {
323
+ langs.sort((a, b) => b.count - a.count);
324
+ }
325
+ return langs;
326
+ }
327
+ };
328
+ }
329
+ </script>
330
+ </body>
331
+ </html>`;
332
+ }
333
+
334
+ /**
335
+ * Spins up a temporary HTTP server, serves the HTML dashboard,
336
+ * and auto-opens the user's default browser.
337
+ *
338
+ * @param stats The aggregated project statistics.
339
+ * @param port The port to serve on (defaults to 0 for auto-assign).
340
+ * @returns A cleanup function to shut down the server.
341
+ */
342
+ export async function serveHtmlDashboard(
343
+ stats: ProjectStats,
344
+ port: number = 0
345
+ ): Promise<{ url: string; close: () => void }> {
346
+ const html = generateHtmlDashboard(stats);
347
+
348
+ return new Promise((resolve, reject) => {
349
+ const server = http.createServer((_req, res) => {
350
+ res.writeHead(200, {
351
+ 'Content-Type': 'text/html; charset=utf-8',
352
+ 'Cache-Control': 'no-store',
353
+ });
354
+ res.end(html);
355
+ });
356
+
357
+ server.listen(port, '127.0.0.1', () => {
358
+ const address = server.address();
359
+ if (!address || typeof address === 'string') {
360
+ reject(new Error('Failed to get server address'));
361
+ return;
362
+ }
363
+
364
+ const url = `http://127.0.0.1:${address.port}`;
365
+
366
+ // Auto-open the default browser (platform-independent)
367
+ const openCmd = process.platform === 'darwin'
368
+ ? `open "${url}"`
369
+ : process.platform === 'win32'
370
+ ? `start "${url}"`
371
+ : `xdg-open "${url}"`;
372
+
373
+ exec(openCmd);
374
+
375
+ resolve({
376
+ url,
377
+ close: () => server.close(),
378
+ });
379
+ });
380
+
381
+ server.on('error', reject);
382
+ });
383
+ }
384
+
385
+ export { generateHtmlDashboard };