headhunt 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,287 @@
1
+ const { C, fmt } = require('./colors');
2
+ const { SCORE_WEIGHTS } = require('./constants');
3
+
4
+ function printBanner() {
5
+ console.log();
6
+ console.log(fmt.cyan(fmt.bold(
7
+ ' ██╗ ██╗███████╗ █████╗ ██████╗ ██╗ ██╗██╗ ██╗███╗ ██╗████████╗ ██████╗██╗ ██╗'
8
+ )));
9
+ console.log(fmt.cyan(
10
+ ' ██║ ██║██╔════╝██╔══██╗██╔══██╗██║ ██║██║ ██║████╗ ██║╚══██╔══╝ ██╔════╝██║ ██║'
11
+ ));
12
+ console.log(fmt.cyan(
13
+ ' ███████║█████╗ ███████║██║ ██║███████║██║ ██║██╔██╗ ██║ ██║█████╗██║ ██║ ██║'
14
+ ));
15
+ console.log(fmt.cyan(
16
+ ' ██╔══██║██╔══╝ ██╔══██║██║ ██║██╔══██║██║ ██║██║╚██╗██║ ██║╚════╝██║ ██║ ██║'
17
+ ));
18
+ console.log(fmt.cyan(
19
+ ' ██║ ██║███████╗██║ ██║██████╔╝██║ ██║╚██████╔╝██║ ╚████║ ██║ ╚██████╗███████╗██║'
20
+ ));
21
+ console.log(fmt.cyan(
22
+ ' ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝╚═╝'
23
+ ));
24
+ console.log(fmt.dim(' Developed by @imharris24'));
25
+ console.log();
26
+ }
27
+
28
+ function line(char = '\u2500', len = 60) {
29
+ return fmt.dim(char.repeat(len));
30
+ }
31
+
32
+ function scoreBar(score, max, width = 20) {
33
+ const pct = score / max;
34
+ const filled = Math.round(pct * width);
35
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
36
+ if (pct >= 0.8) return fmt.brightGreen(bar);
37
+ if (pct >= 0.6) return fmt.yellow(bar);
38
+ if (pct >= 0.4) return fmt.brightYellow(bar);
39
+ return fmt.red(bar);
40
+ }
41
+
42
+ function gradeColor(grade) {
43
+ if (grade.startsWith('A')) return fmt.brightGreen(grade);
44
+ if (grade.startsWith('B')) return fmt.green(grade);
45
+ if (grade.startsWith('C')) return fmt.yellow(grade);
46
+ if (grade.startsWith('D')) return fmt.brightYellow(grade);
47
+ return fmt.red(grade);
48
+ }
49
+
50
+ function scoreColor(score) {
51
+ if (score >= 85) return fmt.brightGreen(score + '/100');
52
+ if (score >= 70) return fmt.green(score + '/100');
53
+ if (score >= 55) return fmt.yellow(score + '/100');
54
+ if (score >= 40) return fmt.brightYellow(score + '/100');
55
+ return fmt.red(score + '/100');
56
+ }
57
+
58
+ function priorityLabel(p) {
59
+ const map = {
60
+ CRITICAL: fmt.brightRed('\u25CF CRITICAL'),
61
+ HIGH: fmt.yellow('\u25CF HIGH '),
62
+ MEDIUM: fmt.brightYellow('\u25CF MEDIUM '),
63
+ LOW: fmt.cyan('\u25CF LOW '),
64
+ };
65
+ return map[p] || p;
66
+ }
67
+
68
+ function impactBadge(impact) {
69
+ if (impact === 'Very High') return `${C.bgRed}${C.bold} VERY HIGH ${C.reset}`;
70
+ if (impact === 'High') return `${C.bgYellow}${C.black}${C.bold} HIGH ${C.reset}`;
71
+ if (impact === 'Medium') return `${C.bgCyan}${C.black}${C.bold} MEDIUM ${C.reset}`;
72
+ return `${C.dim} LOW ${C.reset}`;
73
+ }
74
+
75
+ function effortBadge(effort) {
76
+ if (effort === 'Low') return fmt.brightGreen('Quick fix');
77
+ if (effort === 'Medium') return fmt.yellow('Moderate');
78
+ return fmt.red('Complex');
79
+ }
80
+
81
+ function printReport(meta, options = {}) {
82
+ const s = meta.seo;
83
+ const W = 68;
84
+
85
+ console.log(line('\u2550', W));
86
+ console.log();
87
+ console.log(` ${fmt.bold(fmt.brightCyan('TARGET'))} ${meta.finalUrl}`);
88
+ console.log(` ${fmt.bold(fmt.dim('SCANNED'))} ${new Date(meta.timestamp).toLocaleString()}`);
89
+ if (meta.seo.basic?.cms) console.log(` ${fmt.bold(fmt.dim('PLATFORM'))} ${meta.seo.basic.cms}`);
90
+ console.log();
91
+
92
+ const grade = meta.grade;
93
+ const score = meta.overallScore;
94
+ console.log(line('\u2500', W));
95
+ console.log();
96
+ console.log(` ${fmt.bold('OVERALL SEO SCORE')}`);
97
+ console.log();
98
+ console.log(` ${scoreBar(score, 100, 30)} ${scoreColor(score)} Grade: ${gradeColor(grade)}`);
99
+ console.log();
100
+ console.log(` ${fmt.dim(meta.summary)}`);
101
+ console.log();
102
+ console.log(line('\u2500', W));
103
+
104
+ console.log();
105
+ console.log(` ${fmt.bold('CATEGORY BREAKDOWN')}`);
106
+ console.log();
107
+
108
+ for (const [key, val] of Object.entries(meta.scores)) {
109
+ const label = SCORE_WEIGHTS[key]?.label || key;
110
+ const pct = val.score / val.max;
111
+ const status = pct >= 0.8 ? fmt.brightGreen('\u2713') : pct >= 0.5 ? fmt.yellow('~') : fmt.red('\u2717');
112
+ const padded = label.padEnd(22);
113
+ console.log(` ${status} ${fmt.dim(padded)} ${scoreBar(val.score, val.max, 12)} ${String(val.score).padStart(2)}/${val.max}`);
114
+ }
115
+ console.log();
116
+ console.log(line('\u2500', W));
117
+
118
+ console.log();
119
+ console.log(` ${fmt.bold('QUICK STATS')}`);
120
+ console.log();
121
+
122
+ const basic = s.basic || {};
123
+ const tech = s.technical || {};
124
+ const perf = s.performance || {};
125
+ const cont = s.content || {};
126
+ const img = s.images || {};
127
+ const lnk = s.links || {};
128
+
129
+ const stat = (label, value, highlight = false) => {
130
+ const l = fmt.dim(label.padEnd(26));
131
+ const v = highlight ? fmt.brightCyan(String(value)) : String(value);
132
+ return ` ${l} ${v}`;
133
+ };
134
+
135
+ console.log(stat('Title', basic.title ? `"${(basic.title || '').substring(0, 40)}\u2026" (${basic.titleLength}c)` : fmt.red('MISSING')));
136
+ console.log(stat('Meta Description', basic.metaDescription ? `${basic.descriptionLength} chars` : fmt.red('MISSING')));
137
+ console.log(stat('Canonical', basic.canonical ? fmt.green('Present') : fmt.yellow('Missing')));
138
+ console.log(stat('Language', basic.language || fmt.yellow('Not set')));
139
+ console.log(stat('H1 Tags', s.headings?.counts?.h1 === 1 ? fmt.green('1 (ideal)') : fmt.yellow(String(s.headings?.counts?.h1 || 0))));
140
+ console.log(stat('Word Count', cont.wordCount ? `~${cont.wordCount} words (${cont.estimatedReadingMinutes} min read)` : 'n/a'));
141
+ console.log(stat('Images', img.counts ? `${img.counts.total} total, ${img.counts.altCoverage}% have alt` : 'n/a'));
142
+ console.log(stat('Internal Links', lnk.counts ? String(lnk.counts.internal) : 'n/a'));
143
+ console.log(stat('External Links', lnk.counts ? String(lnk.counts.external) : 'n/a'));
144
+ console.log(stat('Schema Types', s.schema?.schemaTypes?.length ? s.schema.schemaTypes.join(', ').substring(0, 40) : fmt.yellow('None found')));
145
+ console.log(stat('Open Graph Tags', Object.keys(s.openGraph || {}).length ? `${Object.keys(s.openGraph).length} tags` : fmt.yellow('None')));
146
+ console.log(stat('Twitter Card', s.twitter?.card || fmt.yellow('Not set')));
147
+ console.log(stat('HTTPS', tech.https ? fmt.green('Yes') : fmt.red('No')));
148
+ console.log(stat('HTML Size', `${perf.htmlSizeKB || '?'} KB`));
149
+ console.log(stat('Response Time', `${meta.fetchTimeMs}ms`));
150
+ console.log(stat('Server', tech.serverInfo || fmt.dim('hidden')));
151
+
152
+ console.log();
153
+ console.log(line('\u2500', W));
154
+
155
+ const recs = meta.recommendations;
156
+ if (recs.length === 0) {
157
+ console.log();
158
+ console.log(` ${fmt.brightGreen('\uD83C\uDFC6 No major issues found. This page has excellent SEO hygiene.')}`);
159
+ console.log();
160
+ } else {
161
+ console.log();
162
+ console.log(` ${fmt.bold('RECOMMENDATIONS')} ${fmt.dim(`(${recs.length} items)`)}`);
163
+ console.log();
164
+
165
+ const shown = options.audit ? recs : recs.slice(0, 10);
166
+
167
+ for (let i = 0; i < shown.length; i++) {
168
+ const r = shown[i];
169
+ console.log(` ${String(i + 1).padStart(2)}. ${priorityLabel(r.priority)} ${fmt.bold(r.category)}`);
170
+ console.log(` ${fmt.yellow('Issue:')} ${r.issue}`);
171
+ if (options.audit) {
172
+ console.log(` ${fmt.cyan('Fix: ')} ${r.recommendation}`);
173
+ console.log(` ${fmt.dim('Impact:')} ${impactBadge(r.impact)} ${fmt.dim('Effort:')} ${effortBadge(r.effort)}`);
174
+ } else {
175
+ console.log(` ${fmt.cyan('Fix: ')} ${r.recommendation.substring(0, 120)}${r.recommendation.length > 120 ? '\u2026' : ''}`);
176
+ }
177
+ console.log();
178
+ }
179
+
180
+ if (!options.audit && recs.length > 10) {
181
+ console.log(` ${fmt.dim(` \u2026 and ${recs.length - 10} more. Run with --audit for full recommendations.`)}`);
182
+ console.log();
183
+ }
184
+ }
185
+
186
+ console.log(line('\u2500', W));
187
+
188
+ console.log();
189
+ console.log(` ${fmt.bold('TECHNICAL & SECURITY SIGNALS')}`);
190
+ console.log();
191
+
192
+ const check = (label, pass, note = '') => {
193
+ const icon = pass ? fmt.brightGreen('\u2713') : fmt.red('\u2717');
194
+ const l = fmt.dim(label.padEnd(30));
195
+ const n = note ? fmt.dim(` ${note}`) : '';
196
+ console.log(` ${icon} ${l}${n}`);
197
+ };
198
+
199
+ check('HTTPS', tech.https);
200
+ check('HSTS Header', tech.hsts);
201
+ check('X-Frame-Options', tech.xFrameOptions);
202
+ check('X-Content-Type-Options', tech.xContentTypeOptions);
203
+ check('Content Security Policy', tech.csp);
204
+ check('Referrer Policy', tech.referrerPolicy);
205
+ check('Viewport Meta', tech.hasViewport);
206
+ check('Charset Declared', tech.hasCharset);
207
+ check('Favicon', tech.hasFavicon);
208
+ check('Web Manifest', tech.hasManifest);
209
+ check('Sitemap Link', tech.hasSitemap, tech.sitemapUrl ? tech.sitemapUrl.substring(0, 40) : '');
210
+ check('RSS Feed', tech.hasRSS);
211
+ check('AMP Version', tech.hasAMP);
212
+ check('hreflang Tags', (s.basic?.hreflang?.length || 0) > 0, s.basic?.hreflang?.length ? `${s.basic.hreflang.length} locales` : '');
213
+
214
+ if (perf.headScripts > 0) {
215
+ check('Render-blocking Scripts', false, `${perf.headScripts} in <head>`);
216
+ } else {
217
+ check('No Render-blocking Scripts', true);
218
+ }
219
+ check('Deferred/Async Scripts', perf.deferScripts + perf.asyncScripts > 0, `${perf.deferScripts}d + ${perf.asyncScripts}a`);
220
+
221
+ console.log();
222
+ console.log(line('\u2550', W));
223
+ console.log();
224
+ console.log(fmt.dim(` Run with --json-file to save full report | --audit for deep recommendations`));
225
+ console.log();
226
+ }
227
+
228
+ function printComparison(metaA, metaB) {
229
+ const W = 80;
230
+ console.log(line('\u2550', W));
231
+ console.log();
232
+ console.log(` ${fmt.bold('SEO COMPARISON')}`);
233
+ console.log();
234
+ console.log(` A: ${fmt.cyan(metaA.url)}`);
235
+ console.log(` B: ${fmt.cyan(metaB.url)}`);
236
+ console.log();
237
+ console.log(line('\u2500', W));
238
+ console.log();
239
+
240
+ const colA = fmt.bold(' A'.padEnd(10));
241
+ const colB = fmt.bold('B'.padEnd(10));
242
+ console.log(` ${'METRIC'.padEnd(28)} ${colA} ${colB}`);
243
+ console.log(line('\u2500', W));
244
+
245
+ const row = (label, a, b, higherIsBetter = true) => {
246
+ const la = String(a).padEnd(16);
247
+ const lb = String(b);
248
+ let fa = la, fb = lb;
249
+ if (typeof a === 'number' && typeof b === 'number') {
250
+ const aWins = higherIsBetter ? a > b : a < b;
251
+ const bWins = higherIsBetter ? b > a : b < a;
252
+ fa = (aWins ? fmt.brightGreen(la) : (aWins === false && bWins ? fmt.red(la) : la));
253
+ fb = (bWins ? fmt.brightGreen(lb) : (bWins === false && aWins ? fmt.red(lb) : lb));
254
+ }
255
+ console.log(` ${fmt.dim(label.padEnd(28))} ${fa} ${fb}`);
256
+ };
257
+
258
+ row('Overall Score', metaA.overallScore, metaB.overallScore);
259
+ row('Grade', metaA.grade, metaB.grade);
260
+ row('Response Time (ms)', metaA.fetchTimeMs, metaB.fetchTimeMs, false);
261
+ row('HTML Size (KB)', metaA.seo.performance?.htmlSizeKB, metaB.seo.performance?.htmlSizeKB, false);
262
+ row('Word Count', metaA.seo.content?.wordCount, metaB.seo.content?.wordCount);
263
+ row('H1 Count', metaA.seo.headings?.counts?.h1, metaB.seo.headings?.counts?.h1);
264
+ row('Images Total', metaA.seo.images?.counts?.total, metaB.seo.images?.counts?.total);
265
+ row('Alt Coverage %', metaA.seo.images?.counts?.altCoverage, metaB.seo.images?.counts?.altCoverage);
266
+ row('Internal Links', metaA.seo.links?.counts?.internal, metaB.seo.links?.counts?.internal);
267
+ row('Schema Types', metaA.seo.schema?.jsonLd?.length, metaB.seo.schema?.jsonLd?.length);
268
+ row('OG Tags', Object.keys(metaA.seo.openGraph || {}).length, Object.keys(metaB.seo.openGraph || {}).length);
269
+ row('Recommendations', metaA.recommendations.length, metaB.recommendations.length, false);
270
+ row('HTTPS', metaA.seo.technical?.https ? 1 : 0, metaB.seo.technical?.https ? 1 : 0);
271
+
272
+ const winner = metaA.overallScore > metaB.overallScore ? 'A' : metaB.overallScore > metaA.overallScore ? 'B' : 'TIE';
273
+ console.log();
274
+ console.log(line('\u2500', W));
275
+ console.log();
276
+ if (winner === 'TIE') {
277
+ console.log(` ${fmt.yellow('\u2696 RESULT: Both pages are evenly matched.')}`);
278
+ } else {
279
+ console.log(` ${fmt.brightGreen(`\uD83C\uDFC6 WINNER: URL ${winner}`)} \u2014 ${winner === 'A' ? metaA.url : metaB.url}`);
280
+ const diff = Math.abs(metaA.overallScore - metaB.overallScore);
281
+ console.log(` ${fmt.dim(` Margin: ${diff} points (${diff < 5 ? 'very close' : diff < 15 ? 'moderate gap' : 'significant gap'})`)}`);
282
+ }
283
+ console.log();
284
+ console.log(line('\u2550', W));
285
+ }
286
+
287
+ module.exports = { printBanner, line, scoreBar, gradeColor, scoreColor, priorityLabel, impactBadge, effortBadge, printReport, printComparison };