skill-guide 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -3
- package/README.md +114 -28
- package/SKILL.md +25 -251
- package/demo-categories.png +0 -0
- package/demo-cover.png +0 -0
- package/demo-highlights.png +0 -0
- package/demo-reference.png +0 -0
- package/package.json +3 -2
- package/scan-skills.js +228 -12
- package/skill-guide.js +2472 -34
- package/skill-registry.js +288 -0
package/skill-guide.js
CHANGED
|
@@ -8,6 +8,7 @@ const path = require('path');
|
|
|
8
8
|
|
|
9
9
|
const ROOT = __dirname;
|
|
10
10
|
const SCANNER = path.join(ROOT, 'scan-skills.js');
|
|
11
|
+
const registryModule = require('./skill-registry');
|
|
11
12
|
const args = process.argv.slice(2);
|
|
12
13
|
|
|
13
14
|
function hasFlag(flag) {
|
|
@@ -23,17 +24,23 @@ function getArgValue(flag) {
|
|
|
23
24
|
function usage() {
|
|
24
25
|
return [
|
|
25
26
|
'Usage:',
|
|
26
|
-
' skill-guide
|
|
27
|
-
' skill-guide --
|
|
28
|
-
' skill-guide --
|
|
29
|
-
'
|
|
30
|
-
'
|
|
27
|
+
' skill-guide # Dashboard: personality, radar, insights (opens HTML)',
|
|
28
|
+
' skill-guide --find <name|query> # Deep dive or search (opens HTML)',
|
|
29
|
+
' skill-guide --doctor # Quick environment diagnostic',
|
|
30
|
+
'',
|
|
31
|
+
'Options:',
|
|
32
|
+
' --output <file> Write to file instead of default',
|
|
33
|
+
' --format json JSON output (no HTML)',
|
|
34
|
+
' --lang en|zh UI language',
|
|
35
|
+
' --refresh Force re-scan (ignore cache)',
|
|
36
|
+
' --all Show skills from all platforms (default: current platform)',
|
|
37
|
+
' --no-open Do not open HTML in browser',
|
|
31
38
|
'',
|
|
32
39
|
'Examples:',
|
|
33
|
-
' npx skill-guide
|
|
34
|
-
' npx skill-guide --
|
|
35
|
-
' npx skill-guide --
|
|
36
|
-
' npx skill-guide --doctor',
|
|
40
|
+
' npx skill-guide # See your dashboard',
|
|
41
|
+
' npx skill-guide --find investigate # Deep dive into a skill',
|
|
42
|
+
' npx skill-guide --find security # Search for security skills',
|
|
43
|
+
' npx skill-guide --doctor # Check for issues',
|
|
37
44
|
].join('\n');
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -66,6 +73,36 @@ const LABELS = {
|
|
|
66
73
|
category: 'Category',
|
|
67
74
|
description: 'Description',
|
|
68
75
|
triggers: 'Triggers',
|
|
76
|
+
skillRecommendations: 'Skill Recommendations',
|
|
77
|
+
yourSkillStack: 'Your skill stack',
|
|
78
|
+
gapAnalysis: 'Gap Analysis',
|
|
79
|
+
noSkillsInCategory: 'You have no {category} skills installed',
|
|
80
|
+
tryThese: 'Try these',
|
|
81
|
+
overlapAlert: 'Review Candidates',
|
|
82
|
+
skillsInCategory: 'You have {count} skills in "{category}" category',
|
|
83
|
+
considerKeeping: 'Review which ones you actually use',
|
|
84
|
+
popularYoureMissing: 'Skills Mentioned in Directories',
|
|
85
|
+
categoriesCovered: 'categories covered',
|
|
86
|
+
myAiSkillStack: 'My AI Skill Stack',
|
|
87
|
+
sharedBy: 'Shared by {user}',
|
|
88
|
+
poweredBy: 'Powered by skill-guide',
|
|
89
|
+
installSkillGuide: 'Install skill-guide to discover your skills',
|
|
90
|
+
topPicks: 'Top Picks',
|
|
91
|
+
nMore: '+ {count} more',
|
|
92
|
+
ctaHeadline: 'Stop guessing. Start using.',
|
|
93
|
+
ctaSubtext: 'Join developers who discovered skills they never knew they had',
|
|
94
|
+
ctaGithub: 'Star on GitHub',
|
|
95
|
+
strongest: 'Strongest',
|
|
96
|
+
weakest: 'Weakest',
|
|
97
|
+
cleanupOpportunities: 'Review Candidates',
|
|
98
|
+
significantOverlap: 'same-category review candidates',
|
|
99
|
+
mostDocumented: 'Most documented:',
|
|
100
|
+
basedOnCompleteness: 'Based on documentation completeness',
|
|
101
|
+
stackInsights: 'Stack Insights',
|
|
102
|
+
capabilityMap: 'Capability Map',
|
|
103
|
+
gapHint: '{action}',
|
|
104
|
+
scatteredSkills: 'Scattered skills, no idea what you have?',
|
|
105
|
+
manySkillsPain: '{count}+ skills but no idea what you have?',
|
|
69
106
|
},
|
|
70
107
|
zh: {
|
|
71
108
|
yourAgentSkills: '你的 Agent Skills 技能库',
|
|
@@ -92,6 +129,36 @@ const LABELS = {
|
|
|
92
129
|
category: '分类',
|
|
93
130
|
description: '描述',
|
|
94
131
|
triggers: '触发词',
|
|
132
|
+
skillRecommendations: '技能推荐',
|
|
133
|
+
yourSkillStack: '你的技能栈',
|
|
134
|
+
gapAnalysis: '空白分析',
|
|
135
|
+
noSkillsInCategory: '你没有安装 {category} 类技能',
|
|
136
|
+
tryThese: '试试这些',
|
|
137
|
+
overlapAlert: '待复核候选',
|
|
138
|
+
skillsInCategory: '你在 "{category}" 分类下有 {count} 个技能',
|
|
139
|
+
considerKeeping: '请复核哪些技能确实常用',
|
|
140
|
+
popularYoureMissing: '目录中提到的技能',
|
|
141
|
+
categoriesCovered: '个分类已覆盖',
|
|
142
|
+
myAiSkillStack: '我的 AI 技能栈',
|
|
143
|
+
sharedBy: '由 {user} 分享',
|
|
144
|
+
poweredBy: '由 skill-guide 驱动',
|
|
145
|
+
installSkillGuide: '安装 skill-guide 来发现你的技能',
|
|
146
|
+
topPicks: '精选推荐',
|
|
147
|
+
nMore: '+ {count} 更多',
|
|
148
|
+
ctaHeadline: '别再猜了,开始用吧',
|
|
149
|
+
ctaSubtext: '加入已发现隐藏技能的开发者行列',
|
|
150
|
+
ctaGithub: '在 GitHub 上 Star',
|
|
151
|
+
strongest: '最强',
|
|
152
|
+
weakest: '最弱',
|
|
153
|
+
cleanupOpportunities: '待复核候选',
|
|
154
|
+
significantOverlap: '同类技能候选',
|
|
155
|
+
mostDocumented: '文档最完善:',
|
|
156
|
+
basedOnCompleteness: '基于文档完整度',
|
|
157
|
+
stackInsights: '技能栈洞察',
|
|
158
|
+
capabilityMap: '能力图谱',
|
|
159
|
+
gapHint: '{action}',
|
|
160
|
+
scatteredSkills: '技能散落,不知道自己有什么?',
|
|
161
|
+
manySkillsPain: '{count}+ 个技能但不知道自己有什么?',
|
|
95
162
|
},
|
|
96
163
|
};
|
|
97
164
|
|
|
@@ -319,17 +386,17 @@ function te(text) {
|
|
|
319
386
|
function parseMode() {
|
|
320
387
|
if (hasFlag('--help') || hasFlag('-h')) return { mode: 'help' };
|
|
321
388
|
if (hasFlag('--doctor')) return { mode: 'doctor' };
|
|
322
|
-
if (hasFlag('--
|
|
323
|
-
|
|
324
|
-
const skill = getArgValue('--skill');
|
|
325
|
-
if (skill) return { mode: 'skill', value: skill };
|
|
389
|
+
if (hasFlag('--recommend')) return { mode: 'recommend' };
|
|
390
|
+
if (hasFlag('--share')) return { mode: 'share' };
|
|
326
391
|
|
|
327
|
-
|
|
328
|
-
|
|
392
|
+
// --find: unified search + deep dive (also supports legacy --search, --skill)
|
|
393
|
+
const find = getArgValue('--find') || getArgValue('--search') || getArgValue('--skill');
|
|
394
|
+
if (find) return { mode: 'find', value: find };
|
|
329
395
|
|
|
330
|
-
|
|
396
|
+
// Positional arg: treat as --find
|
|
397
|
+
const valueFlags = new Set(['--output', '--find', '--search', '--skill', '--format', '--lang', '--user']);
|
|
331
398
|
const positional = args.find((arg, index) => !arg.startsWith('-') && !valueFlags.has(args[index - 1]));
|
|
332
|
-
if (positional) return { mode: '
|
|
399
|
+
if (positional) return { mode: 'find', value: positional };
|
|
333
400
|
|
|
334
401
|
return { mode: 'list' };
|
|
335
402
|
}
|
|
@@ -340,11 +407,10 @@ function scannerArgsFor(mode) {
|
|
|
340
407
|
|
|
341
408
|
if (mode.mode === 'list' || mode.mode === 'doctor') {
|
|
342
409
|
scannerArgs.push('--list');
|
|
343
|
-
} else if (mode.mode === '
|
|
410
|
+
} else if (mode.mode === 'find') {
|
|
411
|
+
// Try as skill first, fall back to search
|
|
344
412
|
scannerArgs.push('--skill', mode.value);
|
|
345
|
-
} else
|
|
346
|
-
scannerArgs.push('--search', mode.value);
|
|
347
|
-
} else if (mode.mode === 'full') {
|
|
413
|
+
} else {
|
|
348
414
|
scannerArgs.push('--full');
|
|
349
415
|
}
|
|
350
416
|
|
|
@@ -353,7 +419,8 @@ function scannerArgsFor(mode) {
|
|
|
353
419
|
|
|
354
420
|
function runScanner(mode) {
|
|
355
421
|
const args = scannerArgsFor(mode);
|
|
356
|
-
|
|
422
|
+
const needsFullBuffer = mode.mode !== 'list' && mode.mode !== 'doctor';
|
|
423
|
+
if (needsFullBuffer) {
|
|
357
424
|
const result = spawnSync(process.execPath, [SCANNER, ...args], {
|
|
358
425
|
cwd: process.cwd(),
|
|
359
426
|
encoding: 'utf8',
|
|
@@ -469,11 +536,20 @@ function renderCover(data, mode) {
|
|
|
469
536
|
full: t('completeManual'),
|
|
470
537
|
}[mode.mode] || t('discovery');
|
|
471
538
|
|
|
539
|
+
// Add personality for default mode
|
|
540
|
+
const skills = data.skills || [];
|
|
541
|
+
let personalityLine = '';
|
|
542
|
+
if (mode.mode === 'list' && skills.length > 0) {
|
|
543
|
+
const personality = analyzeSkillPersonality(skills);
|
|
544
|
+
personalityLine = `<p class="sub" style="font-size:1.3rem;color:var(--accent);font-weight:600;margin-top:8px">${personality.emoji} ${escapeHtml(personality.type)} · ${lang() === 'zh' ? '本地画像' : 'local profile'}</p>`;
|
|
545
|
+
}
|
|
546
|
+
|
|
472
547
|
return `<section class="slide cover">
|
|
473
548
|
<div class="rv center">
|
|
474
549
|
<div class="kicker" data-i18n="label">${escapeHtml(modeLabel)}</div>
|
|
475
550
|
<h1><span class="grad" data-i18n="label">${escapeHtml(title)}</span></h1>
|
|
476
551
|
<p class="sub">${escapeHtml(data.totalCount || 0)} ${t('skillsScanned')} · ${escapeHtml(subtitle)}</p>
|
|
552
|
+
${personalityLine}
|
|
477
553
|
<div class="stats">${Object.entries(data.sources || {}).map(([source, count]) => `<div class="stat"><b>${count}</b><span data-i18n="label">${escapeHtml(source)}</span></div>`).join('')}</div>
|
|
478
554
|
</div>
|
|
479
555
|
</section>`;
|
|
@@ -498,19 +574,43 @@ function renderCategorySlide(skills) {
|
|
|
498
574
|
}
|
|
499
575
|
|
|
500
576
|
function renderHighlights(skills) {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
577
|
+
const isZh = lang() === 'zh';
|
|
578
|
+
|
|
579
|
+
// Score skills by configuration quality (readiness)
|
|
580
|
+
function readinessScore(s) {
|
|
581
|
+
let score = 0;
|
|
582
|
+
if ((s.description || '').length > 100) score += 20;
|
|
583
|
+
if ((s.description || '').length > 200) score += 10;
|
|
584
|
+
if ((s.allowedTools || []).length > 0) score += 30;
|
|
585
|
+
if ((s.triggers || []).length > 0) score += 20;
|
|
586
|
+
if ((s.tokenCost || 0) > 50) score += 10;
|
|
587
|
+
if ((s.sources || []).length > 1) score += 10;
|
|
588
|
+
return score;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Pick the best skill from each major category
|
|
592
|
+
const groups = groupBy(skills, 'category');
|
|
593
|
+
const topPicks = Object.entries(groups)
|
|
594
|
+
.filter(([, items]) => items.length > 0)
|
|
595
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
596
|
+
.slice(0, 6)
|
|
597
|
+
.map(([category, items]) => {
|
|
598
|
+
const best = [...items].sort((a, b) => readinessScore(b) - readinessScore(a))[0];
|
|
599
|
+
return { ...best, _pickCategory: category };
|
|
600
|
+
});
|
|
504
601
|
|
|
505
602
|
return `<section class="slide">
|
|
506
603
|
<div class="rv wide">
|
|
507
|
-
<h2 data-i18n="label">${
|
|
508
|
-
<
|
|
604
|
+
<h2 data-i18n="label">${isZh ? '每类最佳' : 'Best in Category'}</h2>
|
|
605
|
+
<p class="sub" style="margin-bottom:24px">${isZh
|
|
606
|
+
? '从每个领域中选出配置最完整的技能'
|
|
607
|
+
: 'The best-configured skill from each category'}</p>
|
|
608
|
+
<div class="list">${topPicks.map((skill, index) => `<article class="row">
|
|
509
609
|
<strong>${index + 1}</strong>
|
|
510
610
|
<div>
|
|
511
611
|
<h3>${escapeHtml(skill.name)}</h3>
|
|
512
612
|
<p data-i18n="desc">${te(truncate(skill.description, 180))}</p>
|
|
513
|
-
<div>${categoryBadge(skill.
|
|
613
|
+
<div>${categoryBadge(skill._pickCategory)}${(skill.triggers || []).length > 0 ? `<span class="badge" style="background:rgba(34,197,94,.08);color:var(--accent2);border:1px solid rgba(34,197,94,.15)">${skill.triggers.length} triggers</span>` : ''}${(skill.sources || []).length > 1 ? `<span class="badge" style="background:rgba(129,140,248,.08);color:var(--ab);border:1px solid rgba(129,140,248,.15)">${skill.sources.length} platforms</span>` : ''}</div>
|
|
514
614
|
</div>
|
|
515
615
|
</article>`).join('')}</div>
|
|
516
616
|
</div>
|
|
@@ -568,6 +668,166 @@ function renderSelection(data, mode) {
|
|
|
568
668
|
</section>${renderReference(data.skills.slice(0, 20), t('comparisonReference'))}`;
|
|
569
669
|
}
|
|
570
670
|
|
|
671
|
+
|
|
672
|
+
function renderInsightDashboardSlide(skills) {
|
|
673
|
+
const isZh = lang() === 'zh';
|
|
674
|
+
const health = computeHealthStats(skills);
|
|
675
|
+
const personality = analyzeSkillPersonality(skills);
|
|
676
|
+
const radar = computeRadarScores(skills, health);
|
|
677
|
+
const wrapped = computeWrappedStats(skills, health);
|
|
678
|
+
const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
|
|
679
|
+
const tokenK = (totalTokens / 1000).toFixed(1);
|
|
680
|
+
const pct = Math.round((totalTokens / 200000) * 100 * 100) / 100;
|
|
681
|
+
|
|
682
|
+
return `<section class="slide">
|
|
683
|
+
<div class="rv center">
|
|
684
|
+
<div class="kicker" data-i18n="label">${isZh ? '你的技能画像' : 'YOUR SKILL PROFILE'}</div>
|
|
685
|
+
<h2>${personality.emoji} ${escapeHtml(personality.type)}</h2>
|
|
686
|
+
<p class="sub">${escapeHtml(personality.description)}</p>
|
|
687
|
+
<div class="stats" style="margin:24px 0">
|
|
688
|
+
<div class="stat"><b>${skills.length}</b><span>${isZh ? '技能' : 'skills'}</span></div>
|
|
689
|
+
<div class="stat"><b>${radar.overall}/100</b><span>${isZh ? '健康度' : 'health'}</span></div>
|
|
690
|
+
<div class="stat"><b>${Object.keys(groupBy(skills, 'category')).length}/9</b><span>${isZh ? '领域' : 'categories'}</span></div>
|
|
691
|
+
<div class="stat"><b>~${tokenK}K</b><span>tokens</span></div>
|
|
692
|
+
</div>
|
|
693
|
+
${renderDimensionRadar(radar.dimensions)}
|
|
694
|
+
<p class="sub" style="margin-top:16px;font-size:14px;color:var(--muted)">${isZh
|
|
695
|
+
? `🔤 描述 Token 估算:约为 200K 参考 context 的 ${pct}%`
|
|
696
|
+
: `🔤 Estimated description tokens: ${pct}% of a 200K reference context`}</p>
|
|
697
|
+
</div>
|
|
698
|
+
</section>`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function renderCleanupSlide(skills) {
|
|
702
|
+
const isZh = lang() === 'zh';
|
|
703
|
+
|
|
704
|
+
// Source breakdown
|
|
705
|
+
const userSkills = skills.filter(s => (s.sources || []).some(src => ['claude-user', 'codex-user', 'cc-switch'].includes(src)));
|
|
706
|
+
const pluginSkills = skills.filter(s => (s.sources || []).some(src => ['claude-plugin', 'codex-plugin'].includes(src)));
|
|
707
|
+
const systemSkills = skills.filter(s => (s.sources || []).includes('openai-system'));
|
|
708
|
+
|
|
709
|
+
// Duplicates (same name in user + plugin)
|
|
710
|
+
const nameMap = {};
|
|
711
|
+
for (const s of skills) {
|
|
712
|
+
if (!nameMap[s.name]) nameMap[s.name] = new Set();
|
|
713
|
+
for (const src of (s.sources || [])) nameMap[s.name].add(src);
|
|
714
|
+
}
|
|
715
|
+
const duplicates = Object.entries(nameMap)
|
|
716
|
+
.filter(([, srcs]) =>
|
|
717
|
+
[...srcs].some(s => ['claude-user','codex-user','cc-switch'].includes(s)) &&
|
|
718
|
+
[...srcs].some(s => ['claude-plugin','codex-plugin'].includes(s))
|
|
719
|
+
)
|
|
720
|
+
.map(([name]) => name);
|
|
721
|
+
|
|
722
|
+
// Under-configured
|
|
723
|
+
const wrapped = computeWrappedStats(skills, computeHealthStats(skills));
|
|
724
|
+
const dormant = wrapped.untappedCount || 0;
|
|
725
|
+
const dormantPct = skills.length > 0 ? Math.round((dormant / skills.length) * 100) : 0;
|
|
726
|
+
|
|
727
|
+
const userPct = skills.length > 0 ? Math.round((userSkills.length / skills.length) * 100) : 0;
|
|
728
|
+
const pluginPct = skills.length > 0 ? Math.round((pluginSkills.length / skills.length) * 100) : 0;
|
|
729
|
+
|
|
730
|
+
return `<section class="slide">
|
|
731
|
+
<div class="rv center">
|
|
732
|
+
<div class="kicker" data-i18n="label">${isZh ? '清理指南' : 'CLEANUP GUIDE'}</div>
|
|
733
|
+
<h2>${isZh ? '你的技能从哪来的?' : 'Where did your skills come from?'}</h2>
|
|
734
|
+
<div class="stats" style="margin:20px 0">
|
|
735
|
+
<div class="stat" style="border-color:var(--accent)"><b>${userSkills.length}</b><span>${isZh ? '用户目录来源' : 'user-directory sources'}</span></div>
|
|
736
|
+
<div class="stat"><b>${pluginSkills.length}</b><span>${isZh ? '插件目录来源' : 'plugin-directory sources'}</span></div>
|
|
737
|
+
<div class="stat"><b>${systemSkills.length}</b><span>${isZh ? '系统目录来源' : 'system-directory sources'}</span></div>
|
|
738
|
+
</div>
|
|
739
|
+
<div style="background:var(--bg);border-radius:8px;height:12px;max-width:400px;margin:0 auto 20px;overflow:hidden;display:flex">
|
|
740
|
+
<div style="background:var(--accent);height:100%;width:${userPct}%" title="${isZh ? '用户目录来源' : 'User-directory sources'}"></div>
|
|
741
|
+
<div style="background:var(--ab);height:100%;width:${pluginPct}%" title="${isZh ? '插件目录来源' : 'Plugin-directory sources'}"></div>
|
|
742
|
+
</div>
|
|
743
|
+
|
|
744
|
+
${duplicates.length > 0 ? (() => {
|
|
745
|
+
// Build precise delete commands with paths
|
|
746
|
+
const dupeDetails = duplicates.map(name => {
|
|
747
|
+
const skill = skills.find(s => s.name === name);
|
|
748
|
+
const dir = skill ? skill.dir : '';
|
|
749
|
+
// Expand ~ to actual path hint
|
|
750
|
+
const displayDir = dir || `~/.claude/skills/${name}`;
|
|
751
|
+
return { name, dir: displayDir };
|
|
752
|
+
});
|
|
753
|
+
return `<div style="background:var(--card);border:1px solid rgba(234,179,8,0.3);border-radius:var(--r);padding:16px;max-width:580px;margin:0 auto 16px;text-align:left">
|
|
754
|
+
<p style="margin:0 0 8px;color:#f59e0b;font-weight:600;font-size:14px">⚠️ ${isZh ? `${duplicates.length} 个重复技能` : `${duplicates.length} duplicate skills`}</p>
|
|
755
|
+
<p style="font-size:13px;color:var(--muted);margin:0 0 8px">${isZh
|
|
756
|
+
? '这些技能同时存在于你的目录和插件目录中。请先确认自定义改动和实际使用情况,再决定是否移除用户目录副本。'
|
|
757
|
+
: 'These exist in both your directory and the plugin directory. Review custom changes and actual usage before removing a user copy.'}</p>
|
|
758
|
+
${dupeDetails.map(d => `<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
|
|
759
|
+
<code style="flex:1;padding:4px 8px;background:var(--bg);border-radius:4px;font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(d.dir)}</code>
|
|
760
|
+
<code style="padding:4px 8px;background:var(--bg);border-radius:4px;font-size:12px;color:var(--accent2);cursor:pointer;white-space:nowrap" onclick="copyText('Please review whether the skill at ${d.dir} duplicates a plugin copy. Do not delete anything until I confirm.')">📋 copy</code>
|
|
761
|
+
</div>`).join('')}
|
|
762
|
+
<p style="font-size:12px;color:var(--muted);margin:8px 0 0;font-style:italic">${isZh
|
|
763
|
+
? '💡 复制后粘贴给 Agent,让它先复核来源;确认前不要删除'
|
|
764
|
+
: '💡 Paste to your agent for source review; do not delete anything until you confirm'}</p>
|
|
765
|
+
</div>`;
|
|
766
|
+
})() : ''}
|
|
767
|
+
|
|
768
|
+
${dormant > 0 ? `<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;max-width:580px;margin:0 auto;text-align:left">
|
|
769
|
+
<p style="margin:0 0 8px;color:var(--muted);font-weight:600;font-size:14px">📋 ${isZh ? `${dormant} 个配置不完整(${dormantPct}%)` : `${dormant} under-configured (${dormantPct}%)`}</p>
|
|
770
|
+
<p style="font-size:13px;color:var(--muted);margin:0">${isZh
|
|
771
|
+
? '这些技能缺少描述或触发词,Agent 可能更难发现它们。请先复核内容和使用情况。'
|
|
772
|
+
: 'These lack descriptions or triggers, so agents may have more difficulty discovering them. Review content and usage first.'}</p>
|
|
773
|
+
</div>` : ''}
|
|
774
|
+
</div>
|
|
775
|
+
</section>`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function renderNextStepsSlide(skills) {
|
|
779
|
+
const isZh = lang() === 'zh';
|
|
780
|
+
const sample = (skills || []).filter(s => (s.description || '').length > 100).slice(0, 3);
|
|
781
|
+
const sampleName = sample.length > 0 ? sample[0].name : 'investigate';
|
|
782
|
+
const sampleName2 = sample.length > 1 ? sample[1].name : 'security-audit';
|
|
783
|
+
const searchCmd = `npx skill-guide --find security`;
|
|
784
|
+
const skillCmd = `npx skill-guide --find ${sampleName}`;
|
|
785
|
+
const fullCmd = `npx skill-guide --full`;
|
|
786
|
+
const recommendCmd = `npx skill-guide --recommend`;
|
|
787
|
+
const shareCmd = `npx skill-guide --share`;
|
|
788
|
+
const doctorCmd = `npx skill-guide --doctor`;
|
|
789
|
+
|
|
790
|
+
return `<section class="slide">
|
|
791
|
+
<div class="rv center">
|
|
792
|
+
<div class="kicker" data-i18n="label">${isZh ? '下一步' : 'GO DEEPER'}</div>
|
|
793
|
+
<h2 data-i18n="label">${isZh ? '试试这些命令' : 'Try these commands'}</h2>
|
|
794
|
+
<p class="sub" style="font-size:14px">${isZh ? '点击命令即可复制' : 'Click any command to copy'}</p>
|
|
795
|
+
<div style="max-width:640px;margin:24px auto 0;text-align:left">
|
|
796
|
+
<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
|
|
797
|
+
<p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '🔍 搜索技能' : '🔍 Search for a skill'}</p>
|
|
798
|
+
<code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${searchCmd}')">${searchCmd}</code>
|
|
799
|
+
<p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '试试替换关键词: debug, deploy, design, test...' : 'Replace the keyword: try debug, deploy, design, test...'}</p>
|
|
800
|
+
</div>
|
|
801
|
+
<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
|
|
802
|
+
<p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '📖 深入了解一个技能' : '📖 Deep dive into a skill'}</p>
|
|
803
|
+
<code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${skillCmd}')">${skillCmd}</code>
|
|
804
|
+
<p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? `查看 ${sampleName} 的触发词、使用场景和限制` : `See ${sampleName}\'s triggers, use cases, and limitations`}</p>
|
|
805
|
+
</div>
|
|
806
|
+
<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
|
|
807
|
+
<p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '📊 完整参考手册' : '📊 Full reference'}</p>
|
|
808
|
+
<code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${fullCmd}')">${fullCmd}</code>
|
|
809
|
+
<p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '一页一个技能,完整的使用手册' : 'One page per skill, complete manual'}</p>
|
|
810
|
+
</div>
|
|
811
|
+
<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
|
|
812
|
+
<p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '🌐 在线推荐' : '🌐 Get recommendations'}</p>
|
|
813
|
+
<code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${recommendCmd}')">${recommendCmd}</code>
|
|
814
|
+
<p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '查看目录提及项,并复核同类技能候选' : 'Review directory mentions and same-category candidates'}</p>
|
|
815
|
+
</div>
|
|
816
|
+
<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px">
|
|
817
|
+
<p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '📤 分享你的技能栈' : '📤 Share your skill stack'}</p>
|
|
818
|
+
<code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${shareCmd}')">${shareCmd}</code>
|
|
819
|
+
<p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '生成一个可分享的技能组合页面' : 'Generate a shareable portfolio page of your skills'}</p>
|
|
820
|
+
</div>
|
|
821
|
+
<div style="background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px">
|
|
822
|
+
<p style="margin:0 0 6px;color:var(--accent);font-weight:600;font-size:14px">${isZh ? '🩺 诊断环境' : '🩺 Diagnose environment'}</p>
|
|
823
|
+
<code style="display:block;padding:8px 12px;background:var(--bg);border-radius:6px;font-size:13px;color:var(--accent2);cursor:pointer" onclick="copyText('${doctorCmd}')">${doctorCmd}</code>
|
|
824
|
+
<p style="margin:6px 0 0;font-size:12px;color:var(--muted)">${isZh ? '检查损坏文件、重复技能、路径问题' : 'Check broken files, duplicates, path issues'}</p>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
</section>`;
|
|
829
|
+
}
|
|
830
|
+
|
|
571
831
|
function renderSlides(data, mode) {
|
|
572
832
|
if (data.error) {
|
|
573
833
|
return `${renderCover(data, mode)}<section class="slide"><div class="rv center"><h2>Error</h2><p class="sub">${escapeHtml(data.error)}</p></div></section>`;
|
|
@@ -576,7 +836,7 @@ function renderSlides(data, mode) {
|
|
|
576
836
|
if (mode.mode === 'search') return `${renderCover(data, mode)}${renderSelection(data, mode)}`;
|
|
577
837
|
if (mode.mode === 'skill') return `${renderCover(data, mode)}${renderSkillDetails(data.skills)}`;
|
|
578
838
|
if (mode.mode === 'full') return `${renderCover(data, mode)}${renderCategorySlide(data.skills)}${renderSkillDetails(data.skills)}${renderReference(data.skills, t('completeReference'))}`;
|
|
579
|
-
return `${renderCover(data, mode)}${renderCategorySlide(data.skills)}${renderHighlights(data.skills)}${renderReference(data.skills)}`;
|
|
839
|
+
return `${renderCover(data, mode)}${renderInsightDashboardSlide(data.skills)}${renderCleanupSlide(data.skills)}${renderCategorySlide(data.skills)}${renderHighlights(data.skills)}${renderReference(data.skills)}${renderNextStepsSlide(data.skills)}`;
|
|
580
840
|
}
|
|
581
841
|
|
|
582
842
|
function renderHtml(data, mode) {
|
|
@@ -625,6 +885,7 @@ ${slides}
|
|
|
625
885
|
<nav class="progress" aria-label="${lang() === 'zh' ? '幻灯片导航' : 'Slide navigation'}"></nav>
|
|
626
886
|
<div class="shortcut" aria-hidden="true">↓ ↑ Space</div>
|
|
627
887
|
<script>
|
|
888
|
+
function copyText(t){var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');var b=event&&event.target?event.target.closest('code,button'):null;if(b){var o=b.textContent;b.textContent='Copied!';setTimeout(function(){b.textContent=o},1200)}}catch(e){}document.body.removeChild(ta)}
|
|
628
889
|
const seen=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting)e.target.classList.add('v')}),{threshold:.12});
|
|
629
890
|
document.querySelectorAll('.rv').forEach(el=>seen.observe(el));
|
|
630
891
|
const slides=[...document.querySelectorAll('.slide')];
|
|
@@ -645,6 +906,36 @@ function openFile(file) {
|
|
|
645
906
|
spawnSync(command, argsForOpen, { stdio: 'ignore', detached: true });
|
|
646
907
|
}
|
|
647
908
|
|
|
909
|
+
function shouldAutoOpen() {
|
|
910
|
+
if (hasFlag('--no-open')) return false;
|
|
911
|
+
if (hasFlag('--open')) return true;
|
|
912
|
+
const format = getArgValue('--format');
|
|
913
|
+
if (format === 'json') return false;
|
|
914
|
+
if (!process.stdout.isTTY) return false;
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function detectPlatform() {
|
|
919
|
+
// Only filter when explicitly running inside an agent
|
|
920
|
+
if (process.env.CODEX_AGENT) return 'codex';
|
|
921
|
+
if (process.env.CLAUDE_CODE) return 'claude';
|
|
922
|
+
// Default: show all (CODEX_HOME alone is not enough — tests set it too)
|
|
923
|
+
return 'all';
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function filterSkillsByPlatform(skills, platform) {
|
|
927
|
+
if (platform === 'all') return skills;
|
|
928
|
+
if (platform === 'codex') {
|
|
929
|
+
return skills.filter(s => (s.sources || []).some(src =>
|
|
930
|
+
['codex-user', 'codex-plugin', 'openai-system', 'cc-switch'].includes(src)));
|
|
931
|
+
}
|
|
932
|
+
if (platform === 'claude') {
|
|
933
|
+
return skills.filter(s => (s.sources || []).some(src =>
|
|
934
|
+
['claude-user', 'claude-plugin', 'cc-switch'].includes(src)));
|
|
935
|
+
}
|
|
936
|
+
return skills;
|
|
937
|
+
}
|
|
938
|
+
|
|
648
939
|
function skillRoots() {
|
|
649
940
|
const home = os.homedir();
|
|
650
941
|
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
@@ -738,6 +1029,2049 @@ function printDoctor(data) {
|
|
|
738
1029
|
return lines.join('\n');
|
|
739
1030
|
}
|
|
740
1031
|
|
|
1032
|
+
function formatScannerError(data) {
|
|
1033
|
+
const lines = [
|
|
1034
|
+
data.error || 'Skill not found',
|
|
1035
|
+
`Scanned ${data.totalCount || 0} skills.`,
|
|
1036
|
+
];
|
|
1037
|
+
if ((data.suggestions || []).length > 0) {
|
|
1038
|
+
lines.push('Possible matches:');
|
|
1039
|
+
for (const skill of data.suggestions) {
|
|
1040
|
+
const sources = (skill.sources || []).join(', ');
|
|
1041
|
+
lines.push(` - ${skill.name}${sources ? ` (${sources})` : ''}`);
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
lines.push('No close matches found. Try --search <query> to search descriptions and triggers.');
|
|
1045
|
+
}
|
|
1046
|
+
return lines.join('\n');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function renderRecommendTerminal(data, recommendations) {
|
|
1050
|
+
const lines = [];
|
|
1051
|
+
const totalCategories = new Set(data.skills.map((s) => s.category)).size;
|
|
1052
|
+
|
|
1053
|
+
const groups = groupBy(data.skills, 'category');
|
|
1054
|
+
const categoryCounts = Object.entries(groups)
|
|
1055
|
+
.filter(([cat]) => cat !== 'other')
|
|
1056
|
+
.map(([cat, items]) => ({ cat, count: items.length }))
|
|
1057
|
+
.sort((a, b) => b.count - a.count);
|
|
1058
|
+
|
|
1059
|
+
const strongest = categoryCounts[0];
|
|
1060
|
+
const weakest = categoryCounts[categoryCounts.length - 1];
|
|
1061
|
+
|
|
1062
|
+
lines.push('');
|
|
1063
|
+
lines.push('┌─ skill-guide recommend ─────────────────────┐');
|
|
1064
|
+
lines.push('│ │');
|
|
1065
|
+
lines.push(`│ ${t('yourSkillStack')}: ${data.totalCount} skills, ${totalCategories}/9 ${t('categoriesCovered')}`);
|
|
1066
|
+
lines.push('│ │');
|
|
1067
|
+
|
|
1068
|
+
if (strongest) {
|
|
1069
|
+
lines.push(`│ 💪 ${t('strongest')}: ${strongest.cat} (${strongest.count})`);
|
|
1070
|
+
}
|
|
1071
|
+
if (weakest && weakest !== strongest) {
|
|
1072
|
+
lines.push(`│ ⚠️ ${t('weakest')}: ${weakest.cat} (${weakest.count})`);
|
|
1073
|
+
}
|
|
1074
|
+
lines.push('│');
|
|
1075
|
+
|
|
1076
|
+
const gaps = recommendations.filter((r) => r.type === 'gap');
|
|
1077
|
+
if (gaps.length > 0) {
|
|
1078
|
+
lines.push(`│ ⚠️ ${t('gapAnalysis')} (${gaps.length}):`);
|
|
1079
|
+
for (const gap of gaps) {
|
|
1080
|
+
lines.push(`│ • ${gap.category} — 0 skills`);
|
|
1081
|
+
if (gap.action) lines.push(`│ 💡 ${gap.action}`);
|
|
1082
|
+
}
|
|
1083
|
+
lines.push('│');
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const overlaps = recommendations.filter((r) => r.type === 'overlap');
|
|
1087
|
+
const topOverlaps = [...overlaps].sort((a, b) => b.count - a.count).slice(0, 3);
|
|
1088
|
+
if (topOverlaps.length > 0) {
|
|
1089
|
+
lines.push(`│ 📋 ${t('cleanupOpportunities')}:`);
|
|
1090
|
+
for (const overlap of topOverlaps) {
|
|
1091
|
+
lines.push(`│ • ${overlap.category} (${overlap.count} skills) — ${t('significantOverlap')}`);
|
|
1092
|
+
const top3 = overlap.skills.slice(0, 3);
|
|
1093
|
+
if (overlap.completeness) {
|
|
1094
|
+
top3.forEach((name, i) => {
|
|
1095
|
+
const score = overlap.completeness[i];
|
|
1096
|
+
lines.push(`│ ${i + 1}. ${name} (${score}/100)`);
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
lines.push(`│ ${t('basedOnCompleteness')}`);
|
|
1100
|
+
}
|
|
1101
|
+
lines.push('│');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const popular = recommendations.filter((r) => r.type === 'popular');
|
|
1105
|
+
if (popular.length > 0) {
|
|
1106
|
+
lines.push(`│ 🔥 ${t('popularYoureMissing')}:`);
|
|
1107
|
+
for (const skill of popular.filter((s) => s.url).slice(0, 5)) {
|
|
1108
|
+
lines.push(`│ • ${skill.name} (${skill.message})`);
|
|
1109
|
+
}
|
|
1110
|
+
lines.push('│');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
lines.push('└──────────────────────────────────────────────┘');
|
|
1114
|
+
lines.push('');
|
|
1115
|
+
return lines.join('\n');
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function renderRecommendHTML(data, recommendations, user) {
|
|
1119
|
+
const totalCategories = new Set(data.skills.map((s) => s.category)).size;
|
|
1120
|
+
const gaps = recommendations.filter((r) => r.type === 'gap');
|
|
1121
|
+
const popular = recommendations.filter((r) => r.type === 'popular');
|
|
1122
|
+
const overlaps = recommendations.filter((r) => r.type === 'overlap');
|
|
1123
|
+
|
|
1124
|
+
// Sort overlaps by count descending, take top 3
|
|
1125
|
+
const topOverlaps = [...overlaps].sort((a, b) => b.count - a.count).slice(0, 3);
|
|
1126
|
+
|
|
1127
|
+
const categoryBreakdown = Object.entries(
|
|
1128
|
+
data.skills.reduce((acc, s) => { const c = s.category || 'other'; acc[c] = (acc[c] || 0) + 1; return acc; }, {})
|
|
1129
|
+
).sort((a, b) => b[1] - a[1]);
|
|
1130
|
+
|
|
1131
|
+
const breakdownColors = {
|
|
1132
|
+
testing: '#10b981', design: '#f59e0b', security: '#ef4444', documentation: '#8b5cf6',
|
|
1133
|
+
automation: '#06b6d4', deployment: '#ec4899', 'code-quality': '#14b8a6', development: '#f97316', other: '#6b7280',
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
// Stack overview
|
|
1137
|
+
const categoryCounts = categoryBreakdown.filter(([cat]) => cat !== 'other');
|
|
1138
|
+
const strongest = categoryCounts[0];
|
|
1139
|
+
const weakest = categoryCounts[categoryCounts.length - 1];
|
|
1140
|
+
|
|
1141
|
+
const gapCards = gaps.map((gap) => `
|
|
1142
|
+
<article class="card gap-card">
|
|
1143
|
+
<h3>${escapeHtml(gap.category)}</h3>
|
|
1144
|
+
<p>${escapeHtml(t('noSkillsInCategory').replace('{category}', gap.category))}</p>
|
|
1145
|
+
${gap.action ? `<p class="meta">${escapeHtml(gap.action)}</p>` : ''}
|
|
1146
|
+
${gap.skills.length > 0 ? `<div class="chips">${gap.skills.map((s) =>
|
|
1147
|
+
`<a href="${escapeHtml(s.url || '#')}" class="chip" title="${escapeHtml(s.description)}">${escapeHtml(s.name)}</a>`
|
|
1148
|
+
).join('')}</div>` : ''}
|
|
1149
|
+
</article>
|
|
1150
|
+
`).join('');
|
|
1151
|
+
|
|
1152
|
+
const popularItems = popular.filter((s) => s.url).slice(0, 5).map((skill) => `
|
|
1153
|
+
<article class="card popular-card">
|
|
1154
|
+
<h3>${escapeHtml(skill.name)}</h3>
|
|
1155
|
+
<p>${escapeHtml(skill.description || '')}</p>
|
|
1156
|
+
<p class="meta">${escapeHtml(skill.message)}</p>
|
|
1157
|
+
<a href="${escapeHtml(skill.url)}" class="link">GitHub →</a>
|
|
1158
|
+
</article>
|
|
1159
|
+
`).join('');
|
|
1160
|
+
|
|
1161
|
+
const overlapItems = topOverlaps.map((overlap) => {
|
|
1162
|
+
const skillsWithScores = overlap.skills.map((name, i) => {
|
|
1163
|
+
const score = overlap.completeness ? overlap.completeness[i] : null;
|
|
1164
|
+
return { name, score };
|
|
1165
|
+
});
|
|
1166
|
+
return `
|
|
1167
|
+
<article class="card overlap-card">
|
|
1168
|
+
<h3>${escapeHtml(overlap.category)} <span class="count">${overlap.count}</span></h3>
|
|
1169
|
+
<p>${escapeHtml(t('significantOverlap'))}</p>
|
|
1170
|
+
<p class="meta">${escapeHtml(t('mostDocumented'))}</p>
|
|
1171
|
+
<div class="skill-scores">${skillsWithScores.filter((s) => s.score !== null).map((s) =>
|
|
1172
|
+
`<div class="score-row"><span class="score-name">${escapeHtml(s.name)}</span><span class="score-value">${s.score}/100</span></div>`
|
|
1173
|
+
).join('')}</div>
|
|
1174
|
+
<p class="meta score-label">${escapeHtml(t('basedOnCompleteness'))}</p>
|
|
1175
|
+
<div class="chips">${overlap.skills.map((s) => `<span>${escapeHtml(s)}</span>`).join('')}${
|
|
1176
|
+
overlap.hasMore ? `<span class="chip-more">${escapeHtml(t('nMore').replace('{count}', overlap.remainingCount))}</span>` : ''
|
|
1177
|
+
}</div>
|
|
1178
|
+
</article>
|
|
1179
|
+
`;
|
|
1180
|
+
}).join('');
|
|
1181
|
+
|
|
1182
|
+
return `<!DOCTYPE html>
|
|
1183
|
+
<html lang="en">
|
|
1184
|
+
<head>
|
|
1185
|
+
<meta charset="utf-8">
|
|
1186
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1187
|
+
<title>${escapeHtml(t('skillRecommendations'))}</title>
|
|
1188
|
+
<meta property="og:title" content="${escapeHtml(t('skillRecommendations'))} — skill-guide">
|
|
1189
|
+
<meta property="og:description" content="${escapeHtml(t('yourSkillStack'))}: ${data.totalCount} skills, ${totalCategories}/9 ${t('categoriesCovered')}">
|
|
1190
|
+
<style>
|
|
1191
|
+
:root{--bg:#0f0f23;--card:#1a1a2e;--text:#e0e0e0;--muted:#888;--accent:#7c3aed;--accent2:#06b6d4;--gap:#f59e0b;--overlap:#ef4444;--popular:#10b981}
|
|
1192
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
1193
|
+
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;padding:2rem}
|
|
1194
|
+
.container{max-width:960px;margin:0 auto}
|
|
1195
|
+
h1{font-size:2.5rem;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem}
|
|
1196
|
+
h2{font-size:1.5rem;margin:2rem 0 1rem;color:var(--accent2)}
|
|
1197
|
+
.stats{display:flex;gap:1rem;margin:1rem 0;flex-wrap:wrap}
|
|
1198
|
+
.stat{background:var(--card);padding:1rem 1.5rem;border-radius:12px;text-align:center}
|
|
1199
|
+
.stat b{font-size:2rem;display:block}
|
|
1200
|
+
.overview{background:var(--card);padding:1.5rem;border-radius:12px;margin:1.5rem 0;border:1px solid rgba(255,255,255,0.05)}
|
|
1201
|
+
.overview p{margin:0.5rem 0;font-size:1rem}
|
|
1202
|
+
.overview strong{color:var(--accent)}
|
|
1203
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem}
|
|
1204
|
+
.card{background:var(--card);padding:1.5rem;border-radius:12px;border:1px solid rgba(255,255,255,0.05)}
|
|
1205
|
+
.card h3{margin-bottom:0.5rem;font-size:1.1rem;display:flex;align-items:center;gap:0.5rem}
|
|
1206
|
+
.card .count{background:rgba(124,58,237,0.2);padding:0.15rem 0.5rem;border-radius:999px;font-size:0.8rem;color:var(--accent)}
|
|
1207
|
+
.card p{color:var(--muted);font-size:0.9rem}
|
|
1208
|
+
.card .meta{font-size:0.8rem;margin-top:0.5rem}
|
|
1209
|
+
.gap-card{border-left:3px solid var(--gap)}
|
|
1210
|
+
.overlap-card{border-left:3px solid var(--overlap)}
|
|
1211
|
+
.popular-card{border-left:3px solid var(--popular)}
|
|
1212
|
+
.chips{display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.75rem}
|
|
1213
|
+
.chip{background:rgba(124,58,237,0.2);padding:0.25rem 0.75rem;border-radius:999px;font-size:0.85rem;text-decoration:none;color:var(--accent);transition:background 0.2s}
|
|
1214
|
+
.chip:hover{background:rgba(124,58,237,0.4)}
|
|
1215
|
+
.chip-more{background:rgba(255,255,255,0.05);padding:0.25rem 0.75rem;border-radius:999px;font-size:0.85rem;color:var(--muted)}
|
|
1216
|
+
.link{color:var(--accent2);text-decoration:none;font-size:0.85rem}
|
|
1217
|
+
.link:hover{text-decoration:underline}
|
|
1218
|
+
.skill-scores{margin:0.75rem 0}
|
|
1219
|
+
.score-row{display:flex;justify-content:space-between;padding:0.25rem 0;font-size:0.9rem}
|
|
1220
|
+
.score-name{color:var(--text)}
|
|
1221
|
+
.score-value{color:var(--accent);font-family:monospace}
|
|
1222
|
+
.score-label{color:var(--muted);font-size:0.75rem;font-style:italic}
|
|
1223
|
+
.cta{text-align:center;margin:3rem 0;padding:2rem;background:linear-gradient(135deg,rgba(124,58,237,0.1),rgba(6,182,212,0.1));border-radius:16px}
|
|
1224
|
+
.cta h2{margin:0 0 0.5rem}
|
|
1225
|
+
.cta code{background:var(--card);padding:0.5rem 1rem;border-radius:8px;font-size:1.1rem;display:inline-block;margin:0.5rem 0}
|
|
1226
|
+
.cta a{color:var(--accent);text-decoration:none}
|
|
1227
|
+
.cta-sub{color:var(--muted);margin:0.5rem 0 1.5rem;font-size:1rem}
|
|
1228
|
+
.cta-actions{display:flex;gap:1rem;justify-content:center;margin-top:1.5rem}
|
|
1229
|
+
.cta-btn{display:inline-block;padding:0.75rem 2rem;border-radius:8px;font-weight:600;text-decoration:none;font-size:1rem;transition:transform 0.2s,box-shadow 0.2s}
|
|
1230
|
+
.cta-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(124,58,237,0.3)}
|
|
1231
|
+
.cta-btn.primary{background:linear-gradient(135deg,#7c3aed,#06b6d4);color:#fff}
|
|
1232
|
+
.user-tag{color:var(--muted);font-size:0.9rem;margin-bottom:1rem}
|
|
1233
|
+
.breakdown-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin:1rem 0 0.5rem}
|
|
1234
|
+
.breakdown-segment{min-width:2px;transition:width 0.3s}
|
|
1235
|
+
.breakdown-legend{display:flex;flex-wrap:wrap;gap:0.75rem;margin-bottom:1.5rem}
|
|
1236
|
+
.legend-item{display:flex;align-items:center;gap:0.35rem;font-size:0.8rem;color:var(--muted)}
|
|
1237
|
+
.legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
|
1238
|
+
</style>
|
|
1239
|
+
</head>
|
|
1240
|
+
<body>
|
|
1241
|
+
<div class="container">
|
|
1242
|
+
${user ? `<p class="user-tag">${escapeHtml(t('sharedBy').replace('{user}', user))}</p>` : ''}
|
|
1243
|
+
<h1>${escapeHtml(t('skillRecommendations'))}</h1>
|
|
1244
|
+
<div class="stats">
|
|
1245
|
+
<div class="stat"><b>${data.totalCount}</b><span>${t('skillsScanned')}</span></div>
|
|
1246
|
+
<div class="stat"><b>${totalCategories}/9</b><span>${t('categoriesCovered')}</span></div>
|
|
1247
|
+
</div>
|
|
1248
|
+
<div class="breakdown-bar">${categoryBreakdown.map(([cat, count]) => {
|
|
1249
|
+
const pct = Math.round((count / data.totalCount) * 100);
|
|
1250
|
+
const color = breakdownColors[cat] || '#6b7280';
|
|
1251
|
+
return `<div class="breakdown-segment" style="width:${pct}%;background:${color}" title="${cat}: ${count} (${pct}%)"></div>`;
|
|
1252
|
+
}).join('')}</div>
|
|
1253
|
+
<div class="breakdown-legend">${categoryBreakdown.map(([cat, count]) =>
|
|
1254
|
+
`<span class="legend-item"><span class="legend-dot" style="background:${breakdownColors[cat] || '#6b7280'}"></span>${escapeHtml(cat)} (${count})</span>`
|
|
1255
|
+
).join('')}</div>
|
|
1256
|
+
|
|
1257
|
+
<div class="overview">
|
|
1258
|
+
${strongest ? `<p>💪 <strong>${escapeHtml(t('strongest'))}:</strong> ${escapeHtml(strongest[0])} (${strongest[1]} skills)</p>` : ''}
|
|
1259
|
+
${weakest ? `<p>⚠️ <strong>${escapeHtml(t('weakest'))}:</strong> ${escapeHtml(weakest[0])} (${weakest[1]} skills)</p>` : ''}
|
|
1260
|
+
</div>
|
|
1261
|
+
|
|
1262
|
+
${topOverlaps.length > 0 ? `<h2>${escapeHtml(t('cleanupOpportunities'))}</h2><div class="grid">${overlapItems}</div>` : ''}
|
|
1263
|
+
${popular.length > 0 ? `<h2>🔥 ${escapeHtml(t('popularYoureMissing'))}</h2><div class="grid">${popularItems}</div>` : ''}
|
|
1264
|
+
${gaps.length > 0 ? `<h2>Gap Analysis</h2><div class="grid">${gapCards}</div>` : ''}
|
|
1265
|
+
|
|
1266
|
+
<div class="cta">
|
|
1267
|
+
<h2>${escapeHtml(t('ctaHeadline'))}</h2>
|
|
1268
|
+
<p class="cta-sub">${escapeHtml(t('ctaSubtext'))}</p>
|
|
1269
|
+
<code>npx skill-guide --open</code>
|
|
1270
|
+
<div class="cta-actions">
|
|
1271
|
+
<a href="https://github.com/gtskevin/skill-guide" class="cta-btn primary">${escapeHtml(t('ctaGithub'))}</a>
|
|
1272
|
+
</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
</body>
|
|
1276
|
+
</html>`;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
function renderDimensionRadar(dimensions) {
|
|
1281
|
+
const size = 280;
|
|
1282
|
+
const center = size / 2;
|
|
1283
|
+
const radius = 100;
|
|
1284
|
+
const levels = 5;
|
|
1285
|
+
const n = dimensions.length;
|
|
1286
|
+
|
|
1287
|
+
function polygonPoints(r) {
|
|
1288
|
+
return Array.from({ length: n }, (_, i) => {
|
|
1289
|
+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
|
|
1290
|
+
return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
|
|
1291
|
+
}).join(' ');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const dataPoints = dimensions.map((d, i) => {
|
|
1295
|
+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
|
|
1296
|
+
const r = (d.score / 100) * radius;
|
|
1297
|
+
return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
|
|
1298
|
+
}).join(' ');
|
|
1299
|
+
|
|
1300
|
+
const labels = dimensions.map((d, i) => {
|
|
1301
|
+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
|
|
1302
|
+
const labelR = radius + 22;
|
|
1303
|
+
const x = center + labelR * Math.cos(angle);
|
|
1304
|
+
const y = center + labelR * Math.sin(angle);
|
|
1305
|
+
const anchor = i === 0 || i === n / 2 ? 'middle' : i < n / 2 ? 'start' : 'end';
|
|
1306
|
+
return `<text x="${x}" y="${y}" text-anchor="${anchor}" fill="#94a3b8" font-size="10" font-family="monospace">${d.name}</text>`;
|
|
1307
|
+
}).join('');
|
|
1308
|
+
|
|
1309
|
+
const scoreLabels = dimensions.map((d, i) => {
|
|
1310
|
+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
|
|
1311
|
+
const r = (d.score / 100) * radius;
|
|
1312
|
+
const x = center + (r + 12) * Math.cos(angle);
|
|
1313
|
+
const y = center + (r + 12) * Math.sin(angle);
|
|
1314
|
+
return `<text x="${x}" y="${y}" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">${d.score}</text>`;
|
|
1315
|
+
}).join('');
|
|
1316
|
+
|
|
1317
|
+
return `
|
|
1318
|
+
<svg viewBox="0 0 ${size} ${size}" style="max-width:280px;margin:0 auto;display:block">
|
|
1319
|
+
${Array.from({ length: levels }, (_, i) => {
|
|
1320
|
+
const r = (radius / levels) * (i + 1);
|
|
1321
|
+
return `<polygon points="${polygonPoints(r)}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>`;
|
|
1322
|
+
}).join('')}
|
|
1323
|
+
<polygon points="${dataPoints}" fill="rgba(34,197,94,0.15)" stroke="#22c55e" stroke-width="2"/>
|
|
1324
|
+
${labels}
|
|
1325
|
+
${scoreLabels}
|
|
1326
|
+
</svg>
|
|
1327
|
+
`;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function renderRadarChart(skills) {
|
|
1331
|
+
const CATEGORIES = ['testing', 'design', 'security', 'documentation', 'automation', 'deployment', 'code-quality', 'development'];
|
|
1332
|
+
const LABELS_SHORT = ['Test', 'Design', 'Security', 'Docs', 'Auto', 'Deploy', 'Quality', 'Dev'];
|
|
1333
|
+
|
|
1334
|
+
const counts = {};
|
|
1335
|
+
for (const cat of CATEGORIES) counts[cat] = 0;
|
|
1336
|
+
for (const s of skills) {
|
|
1337
|
+
const cat = s.category || 'other';
|
|
1338
|
+
if (counts[cat] !== undefined) counts[cat]++;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const maxCount = Math.max(...Object.values(counts), 1);
|
|
1342
|
+
const cx = 150, cy = 150, r = 120;
|
|
1343
|
+
const angleStep = (2 * Math.PI) / CATEGORIES.length;
|
|
1344
|
+
|
|
1345
|
+
// Grid rings
|
|
1346
|
+
const rings = [0.25, 0.5, 0.75, 1.0].map((scale) => {
|
|
1347
|
+
const points = CATEGORIES.map((_, i) => {
|
|
1348
|
+
const angle = i * angleStep - Math.PI / 2;
|
|
1349
|
+
const x = cx + r * scale * Math.cos(angle);
|
|
1350
|
+
const y = cy + r * scale * Math.sin(angle);
|
|
1351
|
+
return `${x},${y}`;
|
|
1352
|
+
}).join(' ');
|
|
1353
|
+
return `<polygon points="${points}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>`;
|
|
1354
|
+
}).join('\n ');
|
|
1355
|
+
|
|
1356
|
+
// Data polygon
|
|
1357
|
+
const dataPoints = CATEGORIES.map((cat, i) => {
|
|
1358
|
+
const angle = i * angleStep - Math.PI / 2;
|
|
1359
|
+
const value = counts[cat] / maxCount;
|
|
1360
|
+
const x = cx + r * value * Math.cos(angle);
|
|
1361
|
+
const y = cy + r * value * Math.sin(angle);
|
|
1362
|
+
return `${x},${y}`;
|
|
1363
|
+
}).join(' ');
|
|
1364
|
+
|
|
1365
|
+
// Labels
|
|
1366
|
+
const labels = CATEGORIES.map((cat, i) => {
|
|
1367
|
+
const angle = i * angleStep - Math.PI / 2;
|
|
1368
|
+
const lx = cx + (r + 25) * Math.cos(angle);
|
|
1369
|
+
const ly = cy + (r + 25) * Math.sin(angle);
|
|
1370
|
+
return `<text x="${lx}" y="${ly}" text-anchor="middle" dominant-baseline="middle" fill="#888" font-size="11">${LABELS_SHORT[i]}</text>`;
|
|
1371
|
+
}).join('\n ');
|
|
1372
|
+
|
|
1373
|
+
return `<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" class="radar-chart">
|
|
1374
|
+
${rings}
|
|
1375
|
+
<polygon points="${dataPoints}" fill="rgba(124,58,237,0.2)" stroke="#7c3aed" stroke-width="2"/>
|
|
1376
|
+
${labels}
|
|
1377
|
+
</svg>`;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function generatePersona(skills) {
|
|
1381
|
+
const categories = {};
|
|
1382
|
+
for (const s of skills) {
|
|
1383
|
+
const cat = s.category || 'other';
|
|
1384
|
+
categories[cat] = (categories[cat] || 0) + 1;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const total = skills.length;
|
|
1388
|
+
const personas = [];
|
|
1389
|
+
|
|
1390
|
+
if ((categories.security || 0) / total > 0.15) personas.push('Security Champion');
|
|
1391
|
+
if ((categories.testing || 0) / total > 0.15) personas.push('Quality Engineer');
|
|
1392
|
+
if ((categories.deployment || 0) / total > 0.15) personas.push('DevOps Builder');
|
|
1393
|
+
if ((categories.automation || 0) / total > 0.15) personas.push('Automation Architect');
|
|
1394
|
+
if ((categories.design || 0) / total > 0.15) personas.push('Design System Crafter');
|
|
1395
|
+
if ((categories.documentation || 0) / total > 0.1) personas.push('Documentation Advocate');
|
|
1396
|
+
if ((categories['code-quality'] || 0) / total > 0.1) personas.push('Code Quality Guardian');
|
|
1397
|
+
|
|
1398
|
+
if (personas.length === 0) {
|
|
1399
|
+
if (total > 50) personas.push('Skill Collector');
|
|
1400
|
+
else if (total > 20) personas.push('Full-Stack Explorer');
|
|
1401
|
+
else personas.push('Focused Builder');
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
return personas.slice(0, 2).join(' · ');
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function isGarbage(text) {
|
|
1408
|
+
if (!text || typeof text !== 'string') return true;
|
|
1409
|
+
const trimmed = text.trim();
|
|
1410
|
+
if (trimmed.length <= 2) return true;
|
|
1411
|
+
if (/^[>|](-|\+)?$/.test(trimmed)) return true;
|
|
1412
|
+
if (/^---/.test(trimmed)) return true;
|
|
1413
|
+
if (/^(category|tags|name|description)\s*:/.test(trimmed)) return true;
|
|
1414
|
+
return false;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function capabilityPrefix(count) {
|
|
1418
|
+
if (count >= 20) return 'Extensive coverage.';
|
|
1419
|
+
if (count >= 10) return 'Solid coverage.';
|
|
1420
|
+
if (count >= 3) return 'Some coverage.';
|
|
1421
|
+
return 'Getting started.';
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function renderShareHTML(data, user) {
|
|
1425
|
+
const groups = groupBy(data.skills, 'category');
|
|
1426
|
+
const totalCategories = Object.keys(groups).length;
|
|
1427
|
+
const persona = generatePersona(data.skills);
|
|
1428
|
+
const radarChart = renderRadarChart(data.skills);
|
|
1429
|
+
|
|
1430
|
+
// Capability map: one entry per non-empty category (excluding "other")
|
|
1431
|
+
const capabilityCards = Object.entries(groups)
|
|
1432
|
+
.filter(([cat]) => cat !== 'other')
|
|
1433
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
1434
|
+
.map(([category, items]) => {
|
|
1435
|
+
const prefix = capabilityPrefix(items.length);
|
|
1436
|
+
const example = items.find((s) => s.description) || items[0];
|
|
1437
|
+
const desc = isGarbage(example?.description) ? '' : truncate(example?.description || '', 80);
|
|
1438
|
+
return `
|
|
1439
|
+
<article class="card cap-card">
|
|
1440
|
+
<h3>${escapeHtml(category)} <span class="count">${items.length}</span></h3>
|
|
1441
|
+
<p class="cap-prefix">${escapeHtml(prefix)}</p>
|
|
1442
|
+
${desc ? `<p class="cap-desc">${escapeHtml(desc)}</p>` : ''}
|
|
1443
|
+
${example ? `<p class="cap-example">e.g. ${escapeHtml(example.name)}</p>` : ''}
|
|
1444
|
+
</article>
|
|
1445
|
+
`;
|
|
1446
|
+
}).join('');
|
|
1447
|
+
|
|
1448
|
+
// Stack insights
|
|
1449
|
+
const categoryCounts = Object.entries(groups)
|
|
1450
|
+
.filter(([cat]) => cat !== 'other')
|
|
1451
|
+
.map(([cat, items]) => ({ cat, count: items.length }))
|
|
1452
|
+
.sort((a, b) => b.count - a.count);
|
|
1453
|
+
|
|
1454
|
+
const strongest = categoryCounts[0];
|
|
1455
|
+
const weakest = categoryCounts[categoryCounts.length - 1];
|
|
1456
|
+
|
|
1457
|
+
const ALL_CATS = ['testing', 'design', 'security', 'documentation', 'automation', 'deployment', 'code-quality', 'development'];
|
|
1458
|
+
const missingCats = ALL_CATS.filter((cat) => !groups[cat] || groups[cat].length === 0);
|
|
1459
|
+
const gapCategory = missingCats[0] || (weakest && weakest.count <= 2 ? weakest.cat : null);
|
|
1460
|
+
|
|
1461
|
+
const insightsSection = `
|
|
1462
|
+
<h2>${escapeHtml(t('stackInsights'))}</h2>
|
|
1463
|
+
<div class="insights">
|
|
1464
|
+
${strongest ? `<p class="insight-strong">💪 ${escapeHtml(t('strongest'))}: ${escapeHtml(strongest.cat)} (${strongest.count} skills)</p>` : ''}
|
|
1465
|
+
${gapCategory ? `<p class="insight-gap">⚠️ Gap: ${escapeHtml(gapCategory)} (${groups[gapCategory]?.length || 0} skills)<br><span class="gap-hint">${escapeHtml(t('gapHint').replace('{action}', registryModule.GAP_ACTIONS[gapCategory] || ''))}</span></p>` : ''}
|
|
1466
|
+
<p class="insight-cta">Run <code>--recommend</code> for full analysis</p>
|
|
1467
|
+
</div>
|
|
1468
|
+
`;
|
|
1469
|
+
|
|
1470
|
+
// OG tags
|
|
1471
|
+
const ogTitle = `${escapeHtml(persona)} · ${data.totalCount} AI Skills — skill-guide`;
|
|
1472
|
+
const topCapabilities = Object.entries(groups)
|
|
1473
|
+
.filter(([cat]) => cat !== 'other')
|
|
1474
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
1475
|
+
.slice(0, 3)
|
|
1476
|
+
.map(([, items]) => items[0]?.name)
|
|
1477
|
+
.filter(Boolean);
|
|
1478
|
+
const ogDescription = topCapabilities.length > 0
|
|
1479
|
+
? `I can ${topCapabilities.join(', ')}. Here's my full AI skill stack.`
|
|
1480
|
+
: `${data.totalCount} skills across ${totalCategories} categories`;
|
|
1481
|
+
|
|
1482
|
+
return `<!DOCTYPE html>
|
|
1483
|
+
<html lang="en">
|
|
1484
|
+
<head>
|
|
1485
|
+
<meta charset="utf-8">
|
|
1486
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1487
|
+
<title>${escapeHtml(t('myAiSkillStack'))} — skill-guide</title>
|
|
1488
|
+
<meta property="og:title" content="${ogTitle}">
|
|
1489
|
+
<meta property="og:description" content="${escapeHtml(ogDescription)}">
|
|
1490
|
+
<meta property="og:type" content="website">
|
|
1491
|
+
<style>
|
|
1492
|
+
:root{--bg:#0f0f23;--card:#1a1a2e;--text:#e0e0e0;--muted:#888;--accent:#7c3aed;--accent2:#06b6d4;--pick:#10b981;--gap:#f59e0b}
|
|
1493
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
1494
|
+
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;padding:2rem}
|
|
1495
|
+
.container{max-width:960px;margin:0 auto}
|
|
1496
|
+
.hero{text-align:center;padding:3rem 0}
|
|
1497
|
+
h1{font-size:2.5rem;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem}
|
|
1498
|
+
h2{font-size:1.5rem;margin:2.5rem 0 1rem;color:var(--accent2)}
|
|
1499
|
+
.pain{font-size:1.8rem;color:var(--text);font-weight:700;margin:0.5rem 0}
|
|
1500
|
+
.persona{font-size:1.3rem;color:var(--accent);font-weight:600;margin:0.5rem 0;letter-spacing:0.05em}
|
|
1501
|
+
.user-tag{color:var(--muted);font-size:0.9rem;margin-bottom:0.5rem}
|
|
1502
|
+
.subtitle{color:var(--muted);font-size:1rem}
|
|
1503
|
+
.stats{display:flex;gap:1.5rem;justify-content:center;margin:1.5rem 0}
|
|
1504
|
+
.stat{background:var(--card);padding:1rem 2rem;border-radius:12px;text-align:center;min-width:120px}
|
|
1505
|
+
.stat b{font-size:2.5rem;display:block;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
1506
|
+
.stat span{color:var(--muted);font-size:0.85rem}
|
|
1507
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem}
|
|
1508
|
+
.card{background:var(--card);padding:1.5rem;border-radius:12px;border:1px solid rgba(255,255,255,0.05)}
|
|
1509
|
+
.card h3{margin-bottom:0.5rem;font-size:1.1rem;display:flex;align-items:center;gap:0.5rem}
|
|
1510
|
+
.card .count{background:rgba(124,58,237,0.2);padding:0.15rem 0.5rem;border-radius:999px;font-size:0.8rem;color:var(--accent)}
|
|
1511
|
+
.cap-prefix{color:var(--accent);font-size:0.9rem;font-weight:600;margin:0.25rem 0}
|
|
1512
|
+
.cap-desc{color:var(--muted);font-size:0.9rem}
|
|
1513
|
+
.cap-example{color:var(--muted);font-size:0.8rem;font-style:italic;margin-top:0.5rem}
|
|
1514
|
+
.insights{background:var(--card);padding:1.5rem;border-radius:12px;border:1px solid rgba(255,255,255,0.05)}
|
|
1515
|
+
.insight-strong{color:var(--pick);font-size:1.1rem;margin:0.5rem 0}
|
|
1516
|
+
.insight-gap{color:var(--gap);font-size:1.1rem;margin:0.5rem 0}
|
|
1517
|
+
.gap-hint{color:var(--muted);font-size:0.9rem;font-weight:normal}
|
|
1518
|
+
.insight-cta{color:var(--muted);font-size:0.9rem;margin-top:1rem}
|
|
1519
|
+
.insight-cta code{background:rgba(124,58,237,0.2);padding:0.15rem 0.5rem;border-radius:4px;color:var(--accent)}
|
|
1520
|
+
.radar-container{display:flex;justify-content:center;margin:2rem 0}
|
|
1521
|
+
.radar-chart{width:300px;height:300px}
|
|
1522
|
+
.cta{text-align:center;margin:3rem 0;padding:2.5rem;background:linear-gradient(135deg,rgba(124,58,237,0.1),rgba(6,182,212,0.1));border-radius:16px}
|
|
1523
|
+
.cta h2{margin:0 0 0.5rem}
|
|
1524
|
+
.cta p{color:var(--muted);margin:0.5rem 0}
|
|
1525
|
+
.cta code{background:var(--card);padding:0.5rem 1.5rem;border-radius:8px;font-size:1.2rem;display:inline-block;margin:0.75rem 0;color:var(--accent2)}
|
|
1526
|
+
.cta-sub{color:var(--muted);margin:0.5rem 0 1.5rem;font-size:1rem}
|
|
1527
|
+
.cta-actions{display:flex;gap:1rem;justify-content:center;margin-top:1.5rem}
|
|
1528
|
+
.cta-btn{display:inline-block;padding:0.75rem 2rem;border-radius:8px;font-weight:600;text-decoration:none;font-size:1rem;transition:transform 0.2s,box-shadow 0.2s}
|
|
1529
|
+
.cta-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(124,58,237,0.3)}
|
|
1530
|
+
.cta-btn.primary{background:linear-gradient(135deg,#7c3aed,#06b6d4);color:#fff}
|
|
1531
|
+
footer{text-align:center;padding:2rem 0;color:var(--muted);font-size:0.8rem}
|
|
1532
|
+
</style>
|
|
1533
|
+
</head>
|
|
1534
|
+
<body>
|
|
1535
|
+
<div class="container">
|
|
1536
|
+
<div class="hero">
|
|
1537
|
+
${user ? `<p class="user-tag">${escapeHtml(t('sharedBy').replace('{user}', user))}</p>` : ''}
|
|
1538
|
+
<p class="pain">${escapeHtml(data.totalCount >= 100 ? t('manySkillsPain').replace('{count}', data.totalCount) : t('scatteredSkills'))}</p>
|
|
1539
|
+
<h1>${escapeHtml(t('myAiSkillStack'))}</h1>
|
|
1540
|
+
<p class="persona">${escapeHtml(persona)}</p>
|
|
1541
|
+
<p class="subtitle">${data.totalCount} ${t('skillsScanned')} · ${totalCategories} ${t('categoriesCovered')}</p>
|
|
1542
|
+
<div class="radar-container">${radarChart}</div>
|
|
1543
|
+
<div class="stats">
|
|
1544
|
+
<div class="stat"><b>${data.totalCount}</b><span>${t('skillsScanned')}</span></div>
|
|
1545
|
+
<div class="stat"><b>${totalCategories}</b><span>${t('categoriesCovered')}</span></div>
|
|
1546
|
+
</div>
|
|
1547
|
+
</div>
|
|
1548
|
+
|
|
1549
|
+
<h2>${escapeHtml(t('capabilityMap'))}</h2>
|
|
1550
|
+
<div class="grid">${capabilityCards}</div>
|
|
1551
|
+
|
|
1552
|
+
${insightsSection}
|
|
1553
|
+
|
|
1554
|
+
<div class="cta">
|
|
1555
|
+
<h2>${escapeHtml(t('ctaHeadline'))}</h2>
|
|
1556
|
+
<p class="cta-sub">${escapeHtml(t('ctaSubtext'))}</p>
|
|
1557
|
+
<code>npx skill-guide --open</code>
|
|
1558
|
+
<div class="cta-actions">
|
|
1559
|
+
<a href="https://github.com/gtskevin/skill-guide" class="cta-btn primary">${escapeHtml(t('ctaGithub'))}</a>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
</div>
|
|
1563
|
+
<footer>Generated by skill-guide</footer>
|
|
1564
|
+
</body>
|
|
1565
|
+
</html>`;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function estimateTokens(text) {
|
|
1569
|
+
if (!text) return 0;
|
|
1570
|
+
return Math.ceil(text.length / 4);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function computeHealthStats(skills) {
|
|
1574
|
+
const CONTEXT_WINDOW = 200_000;
|
|
1575
|
+
const DESCRIPTION_BUDGET = 16_000;
|
|
1576
|
+
|
|
1577
|
+
let totalDescriptionLength = 0;
|
|
1578
|
+
let totalTokenEstimate = 0;
|
|
1579
|
+
const securityFlags = [];
|
|
1580
|
+
const duplicates = new Map();
|
|
1581
|
+
|
|
1582
|
+
for (const skill of skills) {
|
|
1583
|
+
const descLen = (skill.description || '').length;
|
|
1584
|
+
totalDescriptionLength += descLen;
|
|
1585
|
+
totalTokenEstimate += estimateTokens(skill.description);
|
|
1586
|
+
|
|
1587
|
+
// Security red flags
|
|
1588
|
+
const content = (skill.description || '').toLowerCase();
|
|
1589
|
+
const flags = [];
|
|
1590
|
+
if (content.includes('curl ') && content.includes(' | ')) flags.push('pipe-from-curl');
|
|
1591
|
+
if (content.includes('eval(') || content.includes('exec(')) flags.push('eval-exec');
|
|
1592
|
+
if (content.includes('api_key') || content.includes('apikey') || content.includes('token')) flags.push('handles-secrets');
|
|
1593
|
+
if (content.includes('rm -rf') || content.includes('rmdir /s')) flags.push('destructive-commands');
|
|
1594
|
+
if (flags.length > 0) {
|
|
1595
|
+
securityFlags.push({ name: skill.name, flags });
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Duplicate detection
|
|
1599
|
+
const normalizedName = skill.name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1600
|
+
if (duplicates.has(normalizedName)) {
|
|
1601
|
+
duplicates.get(normalizedName).push(skill.name);
|
|
1602
|
+
} else {
|
|
1603
|
+
duplicates.set(normalizedName, [skill.name]);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const duplicateGroups = [...duplicates.entries()]
|
|
1608
|
+
.filter(([, names]) => names.length > 1)
|
|
1609
|
+
.map(([normalized, names]) => ({ normalized, names }));
|
|
1610
|
+
|
|
1611
|
+
return {
|
|
1612
|
+
totalSkills: skills.length,
|
|
1613
|
+
totalDescriptionLength,
|
|
1614
|
+
totalTokenEstimate,
|
|
1615
|
+
descriptionBudget: DESCRIPTION_BUDGET,
|
|
1616
|
+
budgetUsedPercent: Math.round((totalDescriptionLength / DESCRIPTION_BUDGET) * 100),
|
|
1617
|
+
staleSkills: [], // Scanner doesn't pass _mdFile to skill-guide
|
|
1618
|
+
securityFlags,
|
|
1619
|
+
duplicateGroups,
|
|
1620
|
+
contextWindowPercent: Math.round((totalTokenEstimate / CONTEXT_WINDOW) * 100 * 100) / 100,
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function renderHealthTerminal(data) {
|
|
1625
|
+
const skills = data.skills || [];
|
|
1626
|
+
const health = computeHealthStats(skills);
|
|
1627
|
+
const personality = analyzeSkillPersonality(skills);
|
|
1628
|
+
const radar = computeRadarScores(skills, health);
|
|
1629
|
+
const topConsumers = renderTopConsumers(skills, 5);
|
|
1630
|
+
const prescriptions = generatePrescription(skills, health);
|
|
1631
|
+
const isZh = lang() === 'zh';
|
|
1632
|
+
|
|
1633
|
+
const scoreColor = (s) => s >= 80 ? '🟢' : s >= 60 ? '🟡' : '🔴';
|
|
1634
|
+
|
|
1635
|
+
const lines = [
|
|
1636
|
+
'╔══════════════════════════════════════════════════════════════╗',
|
|
1637
|
+
'║ Skill Health Report ║',
|
|
1638
|
+
'╚══════════════════════════════════════════════════════════════╝',
|
|
1639
|
+
'',
|
|
1640
|
+
`${scoreColor(radar.overall)} Health Score: ${radar.overall}/100`,
|
|
1641
|
+
'',
|
|
1642
|
+
`${personality.emoji} ${isZh ? '本地画像' : 'Local profile'}: ${personality.type} (${personality.title})`,
|
|
1643
|
+
` ${personality.description}`,
|
|
1644
|
+
'',
|
|
1645
|
+
isZh ? '── 你的数据 ─────────────────────────────────────────────' : '── Your Stats ─────────────────────────────────────────────',
|
|
1646
|
+
` 📦 ${isZh ? '总技能数' : 'Total Skills'}: ${skills.length}`,
|
|
1647
|
+
` 🔤 ${isZh ? '描述 Token 估算' : 'Description Token Estimate'}: ~${(health.totalTokenEstimate / 1000).toFixed(1)}K (${health.contextWindowPercent}% ${isZh ? 'of 200K reference context' : 'of 200K reference context'})`,
|
|
1648
|
+
` 📏 ${isZh ? '本地参考预算使用' : 'Local Reference Budget Usage'}: ${health.budgetUsedPercent}%`,
|
|
1649
|
+
'',
|
|
1650
|
+
];
|
|
1651
|
+
|
|
1652
|
+
// Description estimate
|
|
1653
|
+
const tokenPerSkill = skills.length > 0 ? Math.round(health.totalTokenEstimate / skills.length) : 0;
|
|
1654
|
+
lines.push(isZh ? '── 描述估算 ─────────────────────────────────────────────' : '── Description Estimate ─────────────────────────────────');
|
|
1655
|
+
lines.push(isZh
|
|
1656
|
+
? ` 💡 你的 ${skills.length} 个技能,平均每个 ~${tokenPerSkill} tokens。`
|
|
1657
|
+
: ` 💡 Your ${skills.length} skills average ~${tokenPerSkill} tokens each.`);
|
|
1658
|
+
lines.push(isZh
|
|
1659
|
+
? ` 描述总量估算约为 200K 参考 context 的 ${health.contextWindowPercent}%。`
|
|
1660
|
+
: ` The estimated description total is ${health.contextWindowPercent}% of a 200K reference context.`);
|
|
1661
|
+
lines.push('');
|
|
1662
|
+
|
|
1663
|
+
if (topConsumers.length > 0) {
|
|
1664
|
+
lines.push(isZh ? '── Top 5 最长描述 ───────────────────────────────────────' : '── Top 5 Longest Descriptions ─────────────────────────────');
|
|
1665
|
+
for (const c of topConsumers) {
|
|
1666
|
+
const bar = '█'.repeat(Math.round(c.barWidth / 10)) + '░'.repeat(10 - Math.round(c.barWidth / 10));
|
|
1667
|
+
lines.push(` ${c.rank}. ${c.name} ${bar} ${c.tokenCost.toLocaleString()} tokens`);
|
|
1668
|
+
}
|
|
1669
|
+
lines.push('');
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (prescriptions.length > 0) {
|
|
1673
|
+
lines.push(isZh ? '── 复核提示 ──────────────────────────────────────────────' : '── Review Prompts ─────────────────────────────────────────');
|
|
1674
|
+
for (const p of prescriptions) {
|
|
1675
|
+
lines.push(` ${p.emoji} ${isZh ? p.title : p.titleEn} [${p.impact}]`);
|
|
1676
|
+
lines.push(` ${isZh ? p.description : p.descriptionEn}`);
|
|
1677
|
+
}
|
|
1678
|
+
lines.push('');
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
lines.push(isZh ? '── 五维评分 ──────────────────────────────────────────────' : '── Five Dimensions ────────────────────────────────────────');
|
|
1682
|
+
for (const d of radar.dimensions) {
|
|
1683
|
+
const bar = '█'.repeat(Math.round(d.score / 10)) + '░'.repeat(10 - Math.round(d.score / 10));
|
|
1684
|
+
lines.push(` ${d.name} ${bar} ${d.score}/100`);
|
|
1685
|
+
}
|
|
1686
|
+
lines.push('');
|
|
1687
|
+
lines.push(isZh ? '💡 使用 --open 打开交互式仪表盘,支持一键分享' : '💡 Run with --open for interactive dashboard with shareable report');
|
|
1688
|
+
|
|
1689
|
+
return lines.join('\n');
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function renderDefaultTerminal(skills) {
|
|
1693
|
+
const isZh = lang() === 'zh';
|
|
1694
|
+
const health = computeHealthStats(skills);
|
|
1695
|
+
const personality = analyzeSkillPersonality(skills);
|
|
1696
|
+
const radar = computeRadarScores(skills, health);
|
|
1697
|
+
const wrapped = computeWrappedStats(skills, health);
|
|
1698
|
+
const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
|
|
1699
|
+
const tokenK = (totalTokens / 1000).toFixed(1);
|
|
1700
|
+
const pct = Math.round((totalTokens / 200000) * 100 * 100) / 100;
|
|
1701
|
+
const scoreColor = (s) => s >= 80 ? '🟢' : s >= 60 ? '🟡' : '🔴';
|
|
1702
|
+
const groups = groupBy(skills, 'category');
|
|
1703
|
+
const cats = Object.entries(groups).sort((a, b) => b[1].length - a[1].length);
|
|
1704
|
+
|
|
1705
|
+
const lines = [
|
|
1706
|
+
'╔══════════════════════════════════════════════════════════════╗',
|
|
1707
|
+
isZh
|
|
1708
|
+
? `║ skill-guide · ${skills.length} 个技能 · 本地画像:${personality.type} ║`
|
|
1709
|
+
: `║ skill-guide · ${skills.length} skills · Local profile: ${personality.type} ║`,
|
|
1710
|
+
'╚══════════════════════════════════════════════════════════════╝',
|
|
1711
|
+
'',
|
|
1712
|
+
` ${scoreColor(radar.overall)} ${isZh ? '健康度' : 'Health'}: ${radar.overall}/100`,
|
|
1713
|
+
` ${personality.emoji} ${personality.description}`,
|
|
1714
|
+
'',
|
|
1715
|
+
` 📦 ${skills.length} ${isZh ? '个技能' : 'skills'} · ${cats.length}/9 ${isZh ? '个领域' : 'categories'} · 🔤 ~${tokenK}K ${isZh ? '描述 tokens' : 'description tokens'} (${pct}% ${isZh ? 'of 200K 参考 context' : 'of 200K reference context'})`,
|
|
1716
|
+
` 📍 ${isZh ? '本地画像:仅基于当前扫描结果' : 'Local profile: based only on the current scan'}`,
|
|
1717
|
+
'',
|
|
1718
|
+
];
|
|
1719
|
+
|
|
1720
|
+
// Radar
|
|
1721
|
+
lines.push(isZh ? ' ── 五维雷达 ──────────────────────────────────────────' : ' ── Radar ──────────────────────────────────────────────');
|
|
1722
|
+
for (const d of radar.dimensions) {
|
|
1723
|
+
const bar = '█'.repeat(Math.round(d.score / 10)) + '░'.repeat(10 - Math.round(d.score / 10));
|
|
1724
|
+
lines.push(` ${d.name.padEnd(10)} ${bar} ${d.score}/100`);
|
|
1725
|
+
}
|
|
1726
|
+
lines.push('');
|
|
1727
|
+
|
|
1728
|
+
// Top 3 insights
|
|
1729
|
+
lines.push(isZh ? ' ── 关键洞察 ──────────────────────────────────────────' : ' ── Insights ───────────────────────────────────────────');
|
|
1730
|
+
|
|
1731
|
+
// Source breakdown
|
|
1732
|
+
const userCount = skills.filter(s => (s.sources || []).some(src => ['claude-user', 'codex-user', 'cc-switch'].includes(src))).length;
|
|
1733
|
+
const pluginCount = skills.filter(s => (s.sources || []).some(src => ['claude-plugin', 'codex-plugin'].includes(src))).length;
|
|
1734
|
+
lines.push(isZh
|
|
1735
|
+
? ` 📂 来源: ${userCount} 个用户目录来源 · ${pluginCount} 个插件目录来源`
|
|
1736
|
+
: ` 📂 Sources: ${userCount} user-directory · ${pluginCount} plugin-directory`);
|
|
1737
|
+
|
|
1738
|
+
// Dormant skills
|
|
1739
|
+
const dormant = wrapped.untappedCount || 0;
|
|
1740
|
+
const dormantPct = skills.length > 0 ? Math.round((dormant / skills.length) * 100) : 0;
|
|
1741
|
+
if (dormant > 0) {
|
|
1742
|
+
lines.push(isZh
|
|
1743
|
+
? ` ⚠️ ${dormant} 个技能(${dormantPct}%)元数据较少 — 建议复核描述和触发词`
|
|
1744
|
+
: ` ⚠️ ${dormant} skills (${dormantPct}%) have sparse metadata — review descriptions and triggers`);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Budget
|
|
1748
|
+
if (pct > 5) {
|
|
1749
|
+
lines.push(isZh
|
|
1750
|
+
? ` 🔤 描述 Token 估算约为 200K 参考 context 的 ${pct}%`
|
|
1751
|
+
: ` 🔤 Estimated description tokens: ${pct}% of a 200K reference context`);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Short descriptions are review candidates, not usage signals.
|
|
1755
|
+
if (wrapped.shortestDescriptions && wrapped.shortestDescriptions.length > 0) {
|
|
1756
|
+
lines.push(isZh
|
|
1757
|
+
? ` 🔍 描述最短: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`
|
|
1758
|
+
: ` 🔍 Shortest descriptions: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
lines.push('');
|
|
1762
|
+
lines.push(isZh ? ' 💡 使用 --open 打开完整的交互式报告' : ' 💡 Run --open for the full interactive report');
|
|
1763
|
+
lines.push('');
|
|
1764
|
+
|
|
1765
|
+
return lines.join('\n');
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function renderInsightTerminal(data) {
|
|
1769
|
+
const skills = data.skills || [];
|
|
1770
|
+
const health = computeHealthStats(skills);
|
|
1771
|
+
const personality = analyzeSkillPersonality(skills);
|
|
1772
|
+
const radar = computeRadarScores(skills, health);
|
|
1773
|
+
const wrapped = computeWrappedStats(skills, health);
|
|
1774
|
+
const isZh = lang() === 'zh';
|
|
1775
|
+
const scoreColor = (s) => s >= 80 ? '🟢' : s >= 60 ? '🟡' : '🔴';
|
|
1776
|
+
const groups = groupBy(skills, 'category');
|
|
1777
|
+
const categoryCounts = Object.entries(groups)
|
|
1778
|
+
.filter(([cat]) => cat !== 'other')
|
|
1779
|
+
.map(([cat, items]) => ({ cat, count: items.length }))
|
|
1780
|
+
.sort((a, b) => b.count - a.count);
|
|
1781
|
+
const strongest = categoryCounts[0];
|
|
1782
|
+
const weakest = categoryCounts[categoryCounts.length - 1];
|
|
1783
|
+
|
|
1784
|
+
const lines = [
|
|
1785
|
+
'╔══════════════════════════════════════════════════════════════╗',
|
|
1786
|
+
isZh ? '║ 技能洞察报告 ║'
|
|
1787
|
+
: '║ Skill Insight Report ║',
|
|
1788
|
+
'╚══════════════════════════════════════════════════════════════╝',
|
|
1789
|
+
'',
|
|
1790
|
+
];
|
|
1791
|
+
|
|
1792
|
+
// Health section
|
|
1793
|
+
lines.push(isZh ? '── 健康度 ───────────────────────────────────────────────' : '── Health ─────────────────────────────────────────────────');
|
|
1794
|
+
lines.push(` ${scoreColor(radar.overall)} ${isZh ? '分数' : 'Score'}: ${radar.overall}/100 · ${personality.emoji} ${isZh ? '类型' : 'Type'}: ${personality.type}`);
|
|
1795
|
+
lines.push(` ${personality.description}`);
|
|
1796
|
+
lines.push(` ⚡ ${wrapped.coreCount} ${isZh ? '个元数据较完整' : 'metadata-rich'} | ${wrapped.readyCount} ${isZh ? '个元数据一般' : 'partial metadata'} | ${wrapped.untappedCount} ${isZh ? '个元数据较少' : 'sparse metadata'}`);
|
|
1797
|
+
lines.push('');
|
|
1798
|
+
|
|
1799
|
+
// Radar
|
|
1800
|
+
lines.push(isZh ? '── 五维评分 ─────────────────────────────────────────────' : '── Dimensions ────────────────────────────────────────────');
|
|
1801
|
+
for (const d of radar.dimensions) {
|
|
1802
|
+
const bar = '█'.repeat(Math.round(d.score / 10)) + '░'.repeat(10 - Math.round(d.score / 10));
|
|
1803
|
+
lines.push(` ${d.name} ${bar} ${d.score}/100`);
|
|
1804
|
+
}
|
|
1805
|
+
lines.push('');
|
|
1806
|
+
|
|
1807
|
+
// Budget
|
|
1808
|
+
lines.push(isZh ? '── Token 预算 ───────────────────────────────────────────' : '── Token Budget ──────────────────────────────────────────');
|
|
1809
|
+
lines.push(isZh
|
|
1810
|
+
? ` 📦 ${skills.length} 个技能 · 🔤 ~${(health.totalTokenEstimate / 1000).toFixed(1)}K 描述 tokens(约为 200K 参考 context 的 ${health.contextWindowPercent}%)`
|
|
1811
|
+
: ` 📦 ${skills.length} skills · 🔤 ~${(health.totalTokenEstimate / 1000).toFixed(1)}K description tokens (${health.contextWindowPercent}% of 200K reference context)`);
|
|
1812
|
+
lines.push(isZh
|
|
1813
|
+
? ` 📏 本地参考预算使用: ${health.budgetUsedPercent}%`
|
|
1814
|
+
: ` 📏 Local reference budget usage: ${health.budgetUsedPercent}%`);
|
|
1815
|
+
lines.push('');
|
|
1816
|
+
|
|
1817
|
+
// Gaps / cleanup
|
|
1818
|
+
lines.push(isZh ? '── 优化建议 ─────────────────────────────────────────────' : '── Gaps & Cleanup ────────────────────────────────────────');
|
|
1819
|
+
if (strongest) {
|
|
1820
|
+
lines.push(isZh
|
|
1821
|
+
? ` 💪 最强领域: ${strongest.cat} (${strongest.count})`
|
|
1822
|
+
: ` 💪 Strongest: ${strongest.cat} (${strongest.count})`);
|
|
1823
|
+
}
|
|
1824
|
+
if (weakest && weakest !== strongest) {
|
|
1825
|
+
lines.push(isZh
|
|
1826
|
+
? ` ⚠️ 最弱领域: ${weakest.cat} (${weakest.count})`
|
|
1827
|
+
: ` ⚠️ Weakest: ${weakest.cat} (${weakest.count})`);
|
|
1828
|
+
}
|
|
1829
|
+
if (wrapped.shortestDescriptions.length > 0) {
|
|
1830
|
+
lines.push(isZh
|
|
1831
|
+
? ` 🔍 描述最短: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`
|
|
1832
|
+
: ` 🔍 Shortest descriptions: ${wrapped.shortestDescriptions.slice(0, 3).join(', ')}`);
|
|
1833
|
+
}
|
|
1834
|
+
lines.push('');
|
|
1835
|
+
|
|
1836
|
+
// CTA
|
|
1837
|
+
lines.push(isZh ? '── 下一步 ───────────────────────────────────────────────' : '── Next Steps ────────────────────────────────────────────');
|
|
1838
|
+
lines.push(isZh
|
|
1839
|
+
? ' 💡 使用 --open 生成完整的交互式 HTML 报告'
|
|
1840
|
+
: ' 💡 Run with --open for the full interactive HTML report');
|
|
1841
|
+
lines.push(isZh
|
|
1842
|
+
? ' 🔗 使用 --open 生成可分享的 HTML 报告'
|
|
1843
|
+
: ' 🔗 Use --open to generate a shareable HTML report');
|
|
1844
|
+
lines.push('');
|
|
1845
|
+
|
|
1846
|
+
return lines.join('\n');
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
function renderHealthHTML(data) {
|
|
1851
|
+
const skills = data.skills || [];
|
|
1852
|
+
const health = computeHealthStats(skills);
|
|
1853
|
+
const personality = analyzeSkillPersonality(skills);
|
|
1854
|
+
const radar = computeRadarScores(skills, health);
|
|
1855
|
+
const topConsumers = renderTopConsumers(skills, 10);
|
|
1856
|
+
const prescriptions = generatePrescription(skills, health);
|
|
1857
|
+
|
|
1858
|
+
function renderRadarChart(dimensions) {
|
|
1859
|
+
const size = 200;
|
|
1860
|
+
const center = size / 2;
|
|
1861
|
+
const radius = 80;
|
|
1862
|
+
const levels = 5;
|
|
1863
|
+
|
|
1864
|
+
function pentagonPoints(r) {
|
|
1865
|
+
return Array.from({ length: 5 }, (_, i) => {
|
|
1866
|
+
const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
|
|
1867
|
+
return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
|
|
1868
|
+
}).join(' ');
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const dataPoints = dimensions.map((d, i) => {
|
|
1872
|
+
const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
|
|
1873
|
+
const r = (d.score / 100) * radius;
|
|
1874
|
+
return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
|
|
1875
|
+
}).join(' ');
|
|
1876
|
+
|
|
1877
|
+
const labels = dimensions.map((d, i) => {
|
|
1878
|
+
const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
|
|
1879
|
+
const labelR = radius + 25;
|
|
1880
|
+
const x = center + labelR * Math.cos(angle);
|
|
1881
|
+
const y = center + labelR * Math.sin(angle);
|
|
1882
|
+
const anchor = i === 0 ? 'middle' : i < 3 ? 'start' : 'end';
|
|
1883
|
+
return `<text x="${x}" y="${y}" text-anchor="${anchor}" fill="#94a3b8" font-size="11">${d.name}</text>`;
|
|
1884
|
+
}).join('');
|
|
1885
|
+
|
|
1886
|
+
return `
|
|
1887
|
+
<svg viewBox="0 0 ${size} ${size}" class="radar-chart">
|
|
1888
|
+
${Array.from({ length: levels }, (_, i) => {
|
|
1889
|
+
const r = (radius / levels) * (i + 1);
|
|
1890
|
+
return `<polygon points="${pentagonPoints(r)}" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>`;
|
|
1891
|
+
}).join('')}
|
|
1892
|
+
<polygon points="${dataPoints}" fill="rgba(59,130,246,0.3)" stroke="#3b82f6" stroke-width="2"/>
|
|
1893
|
+
${labels}
|
|
1894
|
+
</svg>
|
|
1895
|
+
`;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function scoreColor(score) {
|
|
1899
|
+
if (score >= 80) return '#22c55e';
|
|
1900
|
+
if (score >= 60) return '#f59e0b';
|
|
1901
|
+
return '#ef4444';
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function scoreLabel(score) {
|
|
1905
|
+
if (score >= 90) return 'A+';
|
|
1906
|
+
if (score >= 80) return 'A';
|
|
1907
|
+
if (score >= 70) return 'B';
|
|
1908
|
+
if (score >= 60) return 'C';
|
|
1909
|
+
if (score >= 50) return 'D';
|
|
1910
|
+
return 'F';
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
return `<!DOCTYPE html>
|
|
1914
|
+
<html lang="${lang()}">
|
|
1915
|
+
<head>
|
|
1916
|
+
<meta charset="utf-8">
|
|
1917
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1918
|
+
<title>Skill Health Report</title>
|
|
1919
|
+
<style>
|
|
1920
|
+
:root {
|
|
1921
|
+
--bg: #0f172a;
|
|
1922
|
+
--card: #1e293b;
|
|
1923
|
+
--text: #e2e8f0;
|
|
1924
|
+
--muted: #94a3b8;
|
|
1925
|
+
--accent: #3b82f6;
|
|
1926
|
+
--good: #22c55e;
|
|
1927
|
+
--warn: #f59e0b;
|
|
1928
|
+
--bad: #ef4444;
|
|
1929
|
+
}
|
|
1930
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1931
|
+
body {
|
|
1932
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
1933
|
+
background: var(--bg);
|
|
1934
|
+
color: var(--text);
|
|
1935
|
+
min-height: 100vh;
|
|
1936
|
+
}
|
|
1937
|
+
.hero {
|
|
1938
|
+
text-align: center;
|
|
1939
|
+
padding: 4rem 2rem;
|
|
1940
|
+
background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%);
|
|
1941
|
+
}
|
|
1942
|
+
.score-circle {
|
|
1943
|
+
width: 180px;
|
|
1944
|
+
height: 180px;
|
|
1945
|
+
border-radius: 50%;
|
|
1946
|
+
display: flex;
|
|
1947
|
+
flex-direction: column;
|
|
1948
|
+
align-items: center;
|
|
1949
|
+
justify-content: center;
|
|
1950
|
+
margin: 0 auto 2rem;
|
|
1951
|
+
border: 4px solid;
|
|
1952
|
+
}
|
|
1953
|
+
.score-number {
|
|
1954
|
+
font-size: 4rem;
|
|
1955
|
+
font-weight: 700;
|
|
1956
|
+
line-height: 1;
|
|
1957
|
+
}
|
|
1958
|
+
.score-label {
|
|
1959
|
+
font-size: 1.5rem;
|
|
1960
|
+
font-weight: 600;
|
|
1961
|
+
margin-top: 0.5rem;
|
|
1962
|
+
}
|
|
1963
|
+
.personality {
|
|
1964
|
+
margin-top: 1rem;
|
|
1965
|
+
}
|
|
1966
|
+
.personality-emoji {
|
|
1967
|
+
font-size: 3rem;
|
|
1968
|
+
}
|
|
1969
|
+
.personality-title {
|
|
1970
|
+
font-size: 1.5rem;
|
|
1971
|
+
font-weight: 600;
|
|
1972
|
+
margin-top: 0.5rem;
|
|
1973
|
+
}
|
|
1974
|
+
.personality-desc {
|
|
1975
|
+
color: var(--muted);
|
|
1976
|
+
max-width: 500px;
|
|
1977
|
+
margin: 1rem auto;
|
|
1978
|
+
line-height: 1.6;
|
|
1979
|
+
}
|
|
1980
|
+
.container {
|
|
1981
|
+
max-width: 1000px;
|
|
1982
|
+
margin: 0 auto;
|
|
1983
|
+
padding: 2rem;
|
|
1984
|
+
}
|
|
1985
|
+
.section {
|
|
1986
|
+
background: var(--card);
|
|
1987
|
+
border-radius: 16px;
|
|
1988
|
+
padding: 2rem;
|
|
1989
|
+
margin-bottom: 2rem;
|
|
1990
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
1991
|
+
}
|
|
1992
|
+
.section-title {
|
|
1993
|
+
font-size: 1.25rem;
|
|
1994
|
+
font-weight: 600;
|
|
1995
|
+
margin-bottom: 1.5rem;
|
|
1996
|
+
display: flex;
|
|
1997
|
+
align-items: center;
|
|
1998
|
+
gap: 0.5rem;
|
|
1999
|
+
}
|
|
2000
|
+
.radar-container {
|
|
2001
|
+
display: flex;
|
|
2002
|
+
justify-content: center;
|
|
2003
|
+
padding: 1rem;
|
|
2004
|
+
}
|
|
2005
|
+
.radar-chart {
|
|
2006
|
+
width: 300px;
|
|
2007
|
+
height: 300px;
|
|
2008
|
+
}
|
|
2009
|
+
.consumer-row {
|
|
2010
|
+
display: flex;
|
|
2011
|
+
align-items: center;
|
|
2012
|
+
gap: 1rem;
|
|
2013
|
+
padding: 0.75rem 0;
|
|
2014
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
2015
|
+
}
|
|
2016
|
+
.consumer-rank {
|
|
2017
|
+
width: 24px;
|
|
2018
|
+
font-weight: 600;
|
|
2019
|
+
color: var(--muted);
|
|
2020
|
+
}
|
|
2021
|
+
.consumer-name {
|
|
2022
|
+
flex: 1;
|
|
2023
|
+
font-weight: 500;
|
|
2024
|
+
}
|
|
2025
|
+
.consumer-bar {
|
|
2026
|
+
width: 120px;
|
|
2027
|
+
height: 8px;
|
|
2028
|
+
background: rgba(255,255,255,0.1);
|
|
2029
|
+
border-radius: 4px;
|
|
2030
|
+
overflow: hidden;
|
|
2031
|
+
}
|
|
2032
|
+
.consumer-fill {
|
|
2033
|
+
height: 100%;
|
|
2034
|
+
border-radius: 4px;
|
|
2035
|
+
background: var(--accent);
|
|
2036
|
+
}
|
|
2037
|
+
.consumer-tokens {
|
|
2038
|
+
width: 80px;
|
|
2039
|
+
text-align: right;
|
|
2040
|
+
font-size: 0.875rem;
|
|
2041
|
+
color: var(--muted);
|
|
2042
|
+
}
|
|
2043
|
+
.prescription-card {
|
|
2044
|
+
background: rgba(255,255,255,0.05);
|
|
2045
|
+
border-radius: 12px;
|
|
2046
|
+
padding: 1.5rem;
|
|
2047
|
+
margin-bottom: 1rem;
|
|
2048
|
+
cursor: pointer;
|
|
2049
|
+
transition: background 0.2s;
|
|
2050
|
+
}
|
|
2051
|
+
.prescription-card:hover {
|
|
2052
|
+
background: rgba(255,255,255,0.08);
|
|
2053
|
+
}
|
|
2054
|
+
.prescription-header {
|
|
2055
|
+
display: flex;
|
|
2056
|
+
align-items: center;
|
|
2057
|
+
gap: 1rem;
|
|
2058
|
+
margin-bottom: 0.5rem;
|
|
2059
|
+
}
|
|
2060
|
+
.prescription-emoji {
|
|
2061
|
+
font-size: 1.5rem;
|
|
2062
|
+
}
|
|
2063
|
+
.prescription-title {
|
|
2064
|
+
font-weight: 600;
|
|
2065
|
+
flex: 1;
|
|
2066
|
+
}
|
|
2067
|
+
.prescription-impact {
|
|
2068
|
+
font-size: 0.75rem;
|
|
2069
|
+
padding: 2px 8px;
|
|
2070
|
+
border-radius: 4px;
|
|
2071
|
+
text-transform: uppercase;
|
|
2072
|
+
}
|
|
2073
|
+
.impact-high { background: rgba(239,68,68,0.2); color: #ef4444; }
|
|
2074
|
+
.impact-medium { background: rgba(245,158,11,0.2); color: #f59e0b; }
|
|
2075
|
+
.impact-low { background: rgba(34,197,94,0.2); color: #22c55e; }
|
|
2076
|
+
.prescription-desc {
|
|
2077
|
+
color: var(--muted);
|
|
2078
|
+
font-size: 0.875rem;
|
|
2079
|
+
}
|
|
2080
|
+
.prescription-items {
|
|
2081
|
+
margin-top: 1rem;
|
|
2082
|
+
display: none;
|
|
2083
|
+
}
|
|
2084
|
+
.prescription-card.expanded .prescription-items {
|
|
2085
|
+
display: block;
|
|
2086
|
+
}
|
|
2087
|
+
.prescription-item {
|
|
2088
|
+
display: flex;
|
|
2089
|
+
align-items: center;
|
|
2090
|
+
gap: 0.75rem;
|
|
2091
|
+
padding: 0.5rem 0;
|
|
2092
|
+
font-size: 0.875rem;
|
|
2093
|
+
}
|
|
2094
|
+
.item-name {
|
|
2095
|
+
font-weight: 500;
|
|
2096
|
+
min-width: 120px;
|
|
2097
|
+
}
|
|
2098
|
+
.item-action {
|
|
2099
|
+
color: var(--muted);
|
|
2100
|
+
}
|
|
2101
|
+
.share-section {
|
|
2102
|
+
text-align: center;
|
|
2103
|
+
padding: 2rem;
|
|
2104
|
+
}
|
|
2105
|
+
.share-btn {
|
|
2106
|
+
background: var(--accent);
|
|
2107
|
+
color: white;
|
|
2108
|
+
border: none;
|
|
2109
|
+
padding: 0.75rem 2rem;
|
|
2110
|
+
border-radius: 8px;
|
|
2111
|
+
font-size: 1rem;
|
|
2112
|
+
font-weight: 600;
|
|
2113
|
+
cursor: pointer;
|
|
2114
|
+
transition: transform 0.2s;
|
|
2115
|
+
}
|
|
2116
|
+
.share-btn:hover {
|
|
2117
|
+
transform: scale(1.05);
|
|
2118
|
+
}
|
|
2119
|
+
.copy-feedback {
|
|
2120
|
+
color: var(--good);
|
|
2121
|
+
margin-top: 1rem;
|
|
2122
|
+
display: none;
|
|
2123
|
+
}
|
|
2124
|
+
.stats-row {
|
|
2125
|
+
display: grid;
|
|
2126
|
+
grid-template-columns: repeat(4, 1fr);
|
|
2127
|
+
gap: 1rem;
|
|
2128
|
+
margin-bottom: 2rem;
|
|
2129
|
+
}
|
|
2130
|
+
.stat-card {
|
|
2131
|
+
background: var(--card);
|
|
2132
|
+
border-radius: 12px;
|
|
2133
|
+
padding: 1.5rem;
|
|
2134
|
+
text-align: center;
|
|
2135
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
2136
|
+
}
|
|
2137
|
+
.stat-number {
|
|
2138
|
+
font-size: 2rem;
|
|
2139
|
+
font-weight: 700;
|
|
2140
|
+
margin-bottom: 0.5rem;
|
|
2141
|
+
}
|
|
2142
|
+
.stat-label {
|
|
2143
|
+
font-size: 0.75rem;
|
|
2144
|
+
color: var(--muted);
|
|
2145
|
+
text-transform: uppercase;
|
|
2146
|
+
}
|
|
2147
|
+
</style>
|
|
2148
|
+
</head>
|
|
2149
|
+
<body>
|
|
2150
|
+
<div class="hero">
|
|
2151
|
+
<div class="score-circle" style="border-color: ${scoreColor(radar.overall)}">
|
|
2152
|
+
<div class="score-number" style="color: ${scoreColor(radar.overall)}">${radar.overall}</div>
|
|
2153
|
+
<div class="score-label" style="color: ${scoreColor(radar.overall)}">${scoreLabel(radar.overall)}</div>
|
|
2154
|
+
</div>
|
|
2155
|
+
<div class="personality">
|
|
2156
|
+
<div class="personality-emoji">${personality.emoji}</div>
|
|
2157
|
+
<div class="personality-title">${personality.type} · ${personality.title}</div>
|
|
2158
|
+
<div class="personality-desc">${personality.description}</div>
|
|
2159
|
+
</div>
|
|
2160
|
+
</div>
|
|
2161
|
+
|
|
2162
|
+
<div class="container">
|
|
2163
|
+
<div class="stats-row">
|
|
2164
|
+
<div class="stat-card">
|
|
2165
|
+
<div class="stat-number">${skills.length}</div>
|
|
2166
|
+
<div class="stat-label">Total Skills</div>
|
|
2167
|
+
</div>
|
|
2168
|
+
<div class="stat-card">
|
|
2169
|
+
<div class="stat-number">~${(health.totalTokenEstimate / 1000).toFixed(1)}K</div>
|
|
2170
|
+
<div class="stat-label">Description Token Estimate</div>
|
|
2171
|
+
</div>
|
|
2172
|
+
<div class="stat-card">
|
|
2173
|
+
<div class="stat-number">${health.contextWindowPercent}%</div>
|
|
2174
|
+
<div class="stat-label">200K Reference Context</div>
|
|
2175
|
+
</div>
|
|
2176
|
+
<div class="stat-card">
|
|
2177
|
+
<div class="stat-number">${health.budgetUsedPercent}%</div>
|
|
2178
|
+
<div class="stat-label">Local Reference Budget</div>
|
|
2179
|
+
</div>
|
|
2180
|
+
</div>
|
|
2181
|
+
|
|
2182
|
+
<div class="section">
|
|
2183
|
+
<div class="section-title">📊 Health Radar</div>
|
|
2184
|
+
<div class="radar-container">
|
|
2185
|
+
${renderRadarChart(radar.dimensions)}
|
|
2186
|
+
</div>
|
|
2187
|
+
</div>
|
|
2188
|
+
|
|
2189
|
+
<div class="section">
|
|
2190
|
+
<div class="section-title">🏆 Top 10 Longest Descriptions</div>
|
|
2191
|
+
${topConsumers.map(c => `
|
|
2192
|
+
<div class="consumer-row">
|
|
2193
|
+
<div class="consumer-rank">${c.rank}</div>
|
|
2194
|
+
<div class="consumer-name">${escapeHtml(c.name)}</div>
|
|
2195
|
+
<div class="consumer-bar">
|
|
2196
|
+
<div class="consumer-fill" style="width: ${c.barWidth}%"></div>
|
|
2197
|
+
</div>
|
|
2198
|
+
<div class="consumer-tokens">${c.tokenCost.toLocaleString()} tokens</div>
|
|
2199
|
+
</div>
|
|
2200
|
+
`).join('')}
|
|
2201
|
+
</div>
|
|
2202
|
+
|
|
2203
|
+
<div class="section">
|
|
2204
|
+
<div class="section-title">📋 Review Prompts</div>
|
|
2205
|
+
${prescriptions.map(p => `
|
|
2206
|
+
<div class="prescription-card" onclick="this.classList.toggle('expanded')">
|
|
2207
|
+
<div class="prescription-header">
|
|
2208
|
+
<div class="prescription-emoji">${p.emoji}</div>
|
|
2209
|
+
<div class="prescription-title">${p.title}</div>
|
|
2210
|
+
<div class="prescription-impact impact-${p.impact}">${p.impact}</div>
|
|
2211
|
+
</div>
|
|
2212
|
+
<div class="prescription-desc">${p.description}</div>
|
|
2213
|
+
<div class="prescription-items">
|
|
2214
|
+
${p.items.map(item => `
|
|
2215
|
+
<div class="prescription-item">
|
|
2216
|
+
<div class="item-name">${escapeHtml(item.name)}</div>
|
|
2217
|
+
<div class="item-action">${item.action}</div>
|
|
2218
|
+
</div>
|
|
2219
|
+
`).join('')}
|
|
2220
|
+
</div>
|
|
2221
|
+
</div>
|
|
2222
|
+
`).join('')}
|
|
2223
|
+
</div>
|
|
2224
|
+
|
|
2225
|
+
<div class="section share-section">
|
|
2226
|
+
<div class="section-title">📤 Share Your Health Report</div>
|
|
2227
|
+
<p style="color: var(--muted); margin-bottom: 1.5rem;">Copy to clipboard and share on social media</p>
|
|
2228
|
+
<button class="share-btn" onclick="copyReport()">Copy Report to Clipboard</button>
|
|
2229
|
+
<div class="copy-feedback" id="copyFeedback">✅ Copied!</div>
|
|
2230
|
+
</div>
|
|
2231
|
+
</div>
|
|
2232
|
+
|
|
2233
|
+
<script>
|
|
2234
|
+
function copyReport() {
|
|
2235
|
+
const report = \`🏆 Skill Health Report
|
|
2236
|
+
|
|
2237
|
+
Score: ${radar.overall}/100 (${scoreLabel(radar.overall)})
|
|
2238
|
+
Local Profile: ${personality.emoji} ${personality.type} · ${personality.title}
|
|
2239
|
+
|
|
2240
|
+
📊 Stats:
|
|
2241
|
+
• Total Skills: ${skills.length}
|
|
2242
|
+
• Description Token Estimate: ~${(health.totalTokenEstimate / 1000).toFixed(1)}K
|
|
2243
|
+
• 200K Reference Context: ${health.contextWindowPercent}%
|
|
2244
|
+
• Local Reference Budget: ${health.budgetUsedPercent}%
|
|
2245
|
+
|
|
2246
|
+
${personality.description}
|
|
2247
|
+
|
|
2248
|
+
Generate your report: npx skill-guide --health --open\`;
|
|
2249
|
+
|
|
2250
|
+
navigator.clipboard.writeText(report).then(() => {
|
|
2251
|
+
const feedback = document.getElementById('copyFeedback');
|
|
2252
|
+
feedback.style.display = 'block';
|
|
2253
|
+
setTimeout(() => feedback.style.display = 'none', 2000);
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
</script>
|
|
2257
|
+
</body>
|
|
2258
|
+
</html>`;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
function analyzeSkillPersonality(skills) {
|
|
2262
|
+
const total = skills.length;
|
|
2263
|
+
const categories = {};
|
|
2264
|
+
let totalTokens = 0;
|
|
2265
|
+
let securityCount = 0;
|
|
2266
|
+
let pluginCount = 0;
|
|
2267
|
+
|
|
2268
|
+
for (const skill of skills) {
|
|
2269
|
+
categories[skill.category] = (categories[skill.category] || 0) + 1;
|
|
2270
|
+
totalTokens += skill.tokenCost || 0;
|
|
2271
|
+
if ((skill.allowedTools || []).length > 0) securityCount++;
|
|
2272
|
+
if ((skill.sources || []).some(s => s.includes('plugin'))) pluginCount++;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
const topCategory = Object.entries(categories).sort((a, b) => b[1] - a[1])[0];
|
|
2276
|
+
const isZh = lang() === 'zh';
|
|
2277
|
+
|
|
2278
|
+
if (total > 100) {
|
|
2279
|
+
return {
|
|
2280
|
+
type: isZh ? '收藏家' : 'Collector',
|
|
2281
|
+
emoji: '🏛️',
|
|
2282
|
+
title: 'The Collector',
|
|
2283
|
+
description: isZh
|
|
2284
|
+
? '本地扫描发现了较大的技能库。建议定期复核描述、来源和实际需要。'
|
|
2285
|
+
: 'The local scan found a large skill inventory. Review descriptions, sources, and actual needs periodically.',
|
|
2286
|
+
advice: isZh ? '建议:定期审视,保留精品。质量 > 数量。' : 'Advice: Review regularly, keep the best. Quality > Quantity.',
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (total < 10) {
|
|
2291
|
+
return {
|
|
2292
|
+
type: isZh ? '极简主义者' : 'Minimalist',
|
|
2293
|
+
emoji: '🧘',
|
|
2294
|
+
title: 'The Minimalist',
|
|
2295
|
+
description: isZh
|
|
2296
|
+
? '本地扫描发现了较小的技能库。建议复核它是否覆盖你实际需要的工作。'
|
|
2297
|
+
: 'The local scan found a small skill inventory. Review whether it covers the work you actually need.',
|
|
2298
|
+
advice: isZh ? '建议:保持精简,但可以探索新领域。' : 'Advice: Stay lean, but explore new domains.',
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
if (securityCount > total * 0.3) {
|
|
2303
|
+
return {
|
|
2304
|
+
type: isZh ? '工具声明较多' : 'Tool-Declared Profile',
|
|
2305
|
+
emoji: '🛡️',
|
|
2306
|
+
title: 'The Tool-Declared Profile',
|
|
2307
|
+
description: isZh
|
|
2308
|
+
? '较多技能声明了可用工具。建议按需复核权限、命令和风险。'
|
|
2309
|
+
: 'Many skills declare allowed tools. Review permissions, commands, and risks where needed.',
|
|
2310
|
+
advice: isZh ? '建议:按需复核权限和命令。' : 'Advice: Review permissions and commands where needed.',
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
if (pluginCount > total * 0.5) {
|
|
2315
|
+
return {
|
|
2316
|
+
type: isZh ? '插件来源较多' : 'Plugin-Heavy Profile',
|
|
2317
|
+
emoji: '🔌',
|
|
2318
|
+
title: 'The Plugin-Heavy Profile',
|
|
2319
|
+
description: isZh
|
|
2320
|
+
? '本地扫描发现较多插件目录来源。建议关注来源、质量和维护状态。'
|
|
2321
|
+
: 'The local scan found many plugin-directory sources. Review provenance, quality, and maintenance status.',
|
|
2322
|
+
advice: isZh ? '建议:关注来源、质量和维护状态。' : 'Advice: Review provenance, quality, and maintenance status.',
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (topCategory && topCategory[1] > total * 0.4) {
|
|
2327
|
+
const categoryNames = {
|
|
2328
|
+
'coding': { zh: '代码工匠', en: 'Code Craftsman' },
|
|
2329
|
+
'testing': { zh: '质量守护者', en: 'Quality Guardian' },
|
|
2330
|
+
'devops': { zh: '部署大师', en: 'DevOps Master' },
|
|
2331
|
+
'documentation': { zh: '文档专家', en: 'Documentation Expert' },
|
|
2332
|
+
'analysis': { zh: '数据分析师', en: 'Data Analyst' },
|
|
2333
|
+
};
|
|
2334
|
+
const cat = categoryNames[topCategory[0]] || { zh: '领域专家', en: 'Domain Expert' };
|
|
2335
|
+
return {
|
|
2336
|
+
type: isZh ? cat.zh : cat.en,
|
|
2337
|
+
emoji: '🎯',
|
|
2338
|
+
title: 'The Specialist',
|
|
2339
|
+
description: isZh
|
|
2340
|
+
? `本地元数据集中在 ${topCategory[0]} 类别。建议复核这种分布是否符合实际需要。`
|
|
2341
|
+
: `Local metadata is concentrated in the ${topCategory[0]} category. Review whether that distribution matches your needs.`,
|
|
2342
|
+
advice: isZh ? '建议:复核类别分布是否符合实际需要。' : 'Advice: Review whether the category distribution matches your needs.',
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
return {
|
|
2347
|
+
type: isZh ? '覆盖较广' : 'Broad Coverage',
|
|
2348
|
+
emoji: '🌟',
|
|
2349
|
+
title: 'The All-Rounder',
|
|
2350
|
+
description: isZh
|
|
2351
|
+
? '本地扫描显示技能分布覆盖多个类别。建议复核是否存在不再需要的来源。'
|
|
2352
|
+
: 'The local scan shows skills across several categories. Review whether any sources are no longer needed.',
|
|
2353
|
+
advice: isZh ? '建议:复核来源和类别分布。' : 'Advice: Review sources and category distribution.',
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function renderTopConsumers(skills, limit = 10) {
|
|
2358
|
+
const sorted = [...skills]
|
|
2359
|
+
.sort((a, b) => (b.tokenCost || 0) - (a.tokenCost || 0))
|
|
2360
|
+
.slice(0, limit);
|
|
2361
|
+
|
|
2362
|
+
const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
|
|
2363
|
+
|
|
2364
|
+
return sorted.map((skill, i) => {
|
|
2365
|
+
const percent = totalTokens > 0 ? ((skill.tokenCost || 0) / totalTokens * 100).toFixed(1) : 0;
|
|
2366
|
+
const barWidth = Math.min(percent * 2, 100);
|
|
2367
|
+
return {
|
|
2368
|
+
rank: i + 1,
|
|
2369
|
+
name: skill.name,
|
|
2370
|
+
tokenCost: skill.tokenCost || 0,
|
|
2371
|
+
percent: parseFloat(percent),
|
|
2372
|
+
barWidth,
|
|
2373
|
+
category: skill.category,
|
|
2374
|
+
};
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function computeRadarScores(skills, health) {
|
|
2379
|
+
const isZh = lang() === 'zh';
|
|
2380
|
+
const tokenScore = Math.max(0, 100 - health.contextWindowPercent * 2);
|
|
2381
|
+
const dupScore = Math.max(0, 100 - health.duplicateGroups.length * 10);
|
|
2382
|
+
const secScore = Math.max(0, 100 - health.securityFlags.length * 10); // Changed from 15 to 10
|
|
2383
|
+
const freshScore = Math.max(0, 100 - health.staleSkills.length * 5);
|
|
2384
|
+
const budgetScore = Math.max(0, 100 - Math.max(0, health.budgetUsedPercent - 100) / 5);
|
|
2385
|
+
|
|
2386
|
+
return {
|
|
2387
|
+
dimensions: [
|
|
2388
|
+
{ name: isZh ? '效率' : 'Efficiency', nameEn: 'Efficiency', score: Math.round(tokenScore) },
|
|
2389
|
+
{ name: isZh ? '组织' : 'Organize', nameEn: 'Organize', score: Math.round(dupScore) },
|
|
2390
|
+
{ name: isZh ? '安全' : 'Security', nameEn: 'Security', score: Math.round(secScore) },
|
|
2391
|
+
{ name: isZh ? '新鲜' : 'Fresh', nameEn: 'Fresh', score: Math.round(freshScore) },
|
|
2392
|
+
{ name: isZh ? '预算' : 'Budget', nameEn: 'Budget', score: Math.round(budgetScore) },
|
|
2393
|
+
],
|
|
2394
|
+
overall: Math.round((tokenScore + dupScore + secScore + freshScore + budgetScore) / 5),
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function computeWrappedStats(skills, health) {
|
|
2399
|
+
const total = skills.length;
|
|
2400
|
+
const categories = {};
|
|
2401
|
+
for (const skill of skills) {
|
|
2402
|
+
categories[skill.category] = (categories[skill.category] || 0) + 1;
|
|
2403
|
+
}
|
|
2404
|
+
const categoryCount = Object.keys(categories).length;
|
|
2405
|
+
const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
|
|
2406
|
+
|
|
2407
|
+
// Category breakdown
|
|
2408
|
+
const categoryBreakdown = Object.entries(categories)
|
|
2409
|
+
.sort((a, b) => b[1] - a[1])
|
|
2410
|
+
.map(([name, count]) => ({
|
|
2411
|
+
name,
|
|
2412
|
+
count,
|
|
2413
|
+
percent: Math.round((count / total) * 100),
|
|
2414
|
+
}));
|
|
2415
|
+
|
|
2416
|
+
// Short descriptions are useful review candidates, but do not imply low usage.
|
|
2417
|
+
const shortestDescriptions = [...skills]
|
|
2418
|
+
.sort((a, b) => (a.tokenCost || 0) - (b.tokenCost || 0))
|
|
2419
|
+
.slice(0, 5)
|
|
2420
|
+
.map(s => s.name);
|
|
2421
|
+
|
|
2422
|
+
// --- Metadata completeness analysis (description + tools + triggers + tokens) ---
|
|
2423
|
+
function computeReadinessScore(skill) {
|
|
2424
|
+
let score = 0;
|
|
2425
|
+
const descLen = (skill.description || '').length;
|
|
2426
|
+
if (descLen > 100) score += 20;
|
|
2427
|
+
if (descLen > 200) score += 10;
|
|
2428
|
+
if (descLen > 400) score += 10;
|
|
2429
|
+
if ((skill.allowedTools || []).length > 0) score += 30;
|
|
2430
|
+
if ((skill.triggers || []).length > 0) score += 20;
|
|
2431
|
+
const tokens = skill.tokenCost || 0;
|
|
2432
|
+
if (tokens > 50) score += 5;
|
|
2433
|
+
if (tokens > 100) score += 5;
|
|
2434
|
+
return score;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
const scored = skills.map(s => ({ ...s, _readiness: computeReadinessScore(s) }));
|
|
2438
|
+
const coreSkills = scored.filter(s => s._readiness >= 50);
|
|
2439
|
+
const readySkills = scored.filter(s => s._readiness >= 20 && s._readiness < 50);
|
|
2440
|
+
const untappedSkills = scored.filter(s => s._readiness < 20);
|
|
2441
|
+
const coreCount = coreSkills.length;
|
|
2442
|
+
const readyCount = readySkills.length;
|
|
2443
|
+
const untappedCount = untappedSkills.length;
|
|
2444
|
+
const corePercent = Math.round((coreCount / total) * 100);
|
|
2445
|
+
const readyPercent = Math.round((readyCount / total) * 100);
|
|
2446
|
+
const untappedPercent = Math.round((untappedCount / total) * 100);
|
|
2447
|
+
|
|
2448
|
+
// Top categories for display (still useful for archetype)
|
|
2449
|
+
const sortedCats = Object.entries(categories).sort((a, b) => b[1] - a[1]);
|
|
2450
|
+
|
|
2451
|
+
// --- Local inventory profile ---
|
|
2452
|
+
const topCatPercent = sortedCats.length > 0 ? Math.round((sortedCats[0][1] / total) * 100) : 0;
|
|
2453
|
+
let archetype;
|
|
2454
|
+
if (total <= 3) {
|
|
2455
|
+
archetype = { name: 'Small Inventory', emoji: '🌱', tagline: 'A few local skills were detected' };
|
|
2456
|
+
} else if (total <= 10) {
|
|
2457
|
+
archetype = { name: 'Compact Inventory', emoji: '🔍', tagline: 'A compact set of local skills was detected' };
|
|
2458
|
+
} else if (topCatPercent >= 60 && total >= 20) {
|
|
2459
|
+
archetype = { name: 'Category-Focused Inventory', emoji: '🎯', tagline: 'Local metadata is concentrated in one category' };
|
|
2460
|
+
} else if (total >= 200 && categoryCount >= 7) {
|
|
2461
|
+
archetype = { name: 'Large Broad Inventory', emoji: '🏗️', tagline: 'Many local skills span several categories' };
|
|
2462
|
+
} else if (total >= 200) {
|
|
2463
|
+
archetype = { name: 'Large Inventory', emoji: '⚡', tagline: 'Many local skills were detected' };
|
|
2464
|
+
} else if (categoryCount >= 6 && total < 100) {
|
|
2465
|
+
archetype = { name: 'Broad Inventory', emoji: '🧭', tagline: 'Local skills span several categories' };
|
|
2466
|
+
} else if (total >= 50 && categoryCount <= 4) {
|
|
2467
|
+
archetype = { name: 'Focused Inventory', emoji: '🔬', tagline: 'Many local skills are concentrated in a few categories' };
|
|
2468
|
+
} else {
|
|
2469
|
+
archetype = { name: 'Balanced Inventory', emoji: '⚖️', tagline: 'Local skills are distributed across categories' };
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// Core categories from high-readiness skills
|
|
2473
|
+
const coreCatsFromScore = {};
|
|
2474
|
+
for (const s of coreSkills) {
|
|
2475
|
+
coreCatsFromScore[s.category] = (coreCatsFromScore[s.category] || 0) + 1;
|
|
2476
|
+
}
|
|
2477
|
+
const coreCats = Object.entries(coreCatsFromScore)
|
|
2478
|
+
.sort((a, b) => b[1] - a[1])
|
|
2479
|
+
.slice(0, 3)
|
|
2480
|
+
.map(([name]) => name);
|
|
2481
|
+
if (coreCats.length === 0 && sortedCats.length > 0) {
|
|
2482
|
+
coreCats.push(...sortedCats.slice(0, 2).map(([name]) => name));
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
return {
|
|
2486
|
+
total,
|
|
2487
|
+
categoryCount,
|
|
2488
|
+
totalTokens,
|
|
2489
|
+
categoryBreakdown,
|
|
2490
|
+
shortestDescriptions,
|
|
2491
|
+
coreCount,
|
|
2492
|
+
readyCount,
|
|
2493
|
+
untappedCount,
|
|
2494
|
+
corePercent,
|
|
2495
|
+
readyPercent,
|
|
2496
|
+
untappedPercent,
|
|
2497
|
+
coreCats,
|
|
2498
|
+
archetype,
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function renderWrappedTerminal(data) {
|
|
2503
|
+
const skills = data.skills || [];
|
|
2504
|
+
const health = computeHealthStats(skills);
|
|
2505
|
+
const personality = analyzeSkillPersonality(skills);
|
|
2506
|
+
const wrapped = computeWrappedStats(skills, health);
|
|
2507
|
+
const isZh = lang() === 'zh';
|
|
2508
|
+
|
|
2509
|
+
const lines = [
|
|
2510
|
+
'╔══════════════════════════════════════════════════════════════╗',
|
|
2511
|
+
isZh ? '║ 你的 AI 技能报告 ║'
|
|
2512
|
+
: '║ Your AI Skill Report ║',
|
|
2513
|
+
'╚══════════════════════════════════════════════════════════════╝',
|
|
2514
|
+
'',
|
|
2515
|
+
`${wrapped.archetype.emoji} ${isZh ? '本地技能画像' : 'Local Inventory Profile'}: ${wrapped.archetype.name}`,
|
|
2516
|
+
` ${isZh ? '本地元数据特征:' : 'Local metadata pattern:'} ${wrapped.archetype.tagline}`,
|
|
2517
|
+
'',
|
|
2518
|
+
isZh ? '── 你的数据 ─────────────────────────────────────────────' : '── Your Stats ─────────────────────────────────────────────',
|
|
2519
|
+
` 📦 ${isZh ? '总技能数' : 'Total Skills'}: ${wrapped.total}`,
|
|
2520
|
+
` 📂 ${isZh ? '覆盖领域' : 'Categories'}: ${wrapped.categoryCount}`,
|
|
2521
|
+
` 🔤 ${isZh ? '描述 Token 估算' : 'Description Token Estimate'}: ~${(wrapped.totalTokens / 1000).toFixed(1)}K`,
|
|
2522
|
+
'',
|
|
2523
|
+
isZh ? '── 元数据完整度 ───────────────────────────────────────' : '── Metadata Completeness ──────────────────────────────────',
|
|
2524
|
+
isZh
|
|
2525
|
+
? ` ⚡ ${wrapped.total} 个技能中,${wrapped.coreCount} 个元数据较完整`
|
|
2526
|
+
: ` ⚡ ${wrapped.coreCount} of ${wrapped.total} skills have richer metadata`,
|
|
2527
|
+
isZh
|
|
2528
|
+
? ` ${wrapped.readyCount} 个元数据一般 | ${wrapped.untappedCount} 个元数据较少`
|
|
2529
|
+
: ` ${wrapped.readyCount} partial metadata | ${wrapped.untappedCount} sparse metadata`,
|
|
2530
|
+
isZh
|
|
2531
|
+
? ` 🎯 核心领域: ${wrapped.coreCats.join(', ')}`
|
|
2532
|
+
: ` 🎯 Core domains: ${wrapped.coreCats.join(', ')}`,
|
|
2533
|
+
'',
|
|
2534
|
+
];
|
|
2535
|
+
|
|
2536
|
+
// Category breakdown
|
|
2537
|
+
lines.push(isZh ? '── 技能 DNA ─────────────────────────────────────────────' : '── Skill DNA ─────────────────────────────────────────────');
|
|
2538
|
+
for (const cat of wrapped.categoryBreakdown.slice(0, 6)) {
|
|
2539
|
+
const bar = '█'.repeat(Math.max(1, Math.round(cat.percent / 5)));
|
|
2540
|
+
lines.push(` ${cat.name.padEnd(15)} ${bar} ${cat.count} (${cat.percent}%)`);
|
|
2541
|
+
}
|
|
2542
|
+
lines.push('');
|
|
2543
|
+
|
|
2544
|
+
// Local review candidates
|
|
2545
|
+
lines.push(isZh ? '── 本地复核提示 ─────────────────────────────────────────' : '── Local Review Notes ─────────────────────────────────────');
|
|
2546
|
+
if (wrapped.shortestDescriptions.length > 0) {
|
|
2547
|
+
lines.push(isZh
|
|
2548
|
+
? ` 🔍 描述最短的技能: ${wrapped.shortestDescriptions[0]}`
|
|
2549
|
+
: ` 🔍 Shortest skill description: ${wrapped.shortestDescriptions[0]}`);
|
|
2550
|
+
}
|
|
2551
|
+
lines.push(isZh
|
|
2552
|
+
? ' 📍 以上结论仅基于本地扫描结果,不代表社区排名或实际使用频率'
|
|
2553
|
+
: ' 📍 These notes use local scan data only; they are not community rankings or usage metrics');
|
|
2554
|
+
lines.push('');
|
|
2555
|
+
|
|
2556
|
+
// CTA with suspense-driven share text
|
|
2557
|
+
lines.push(isZh ? '── 分享你的报告 ─────────────────────────────────────────' : '── Share Your Report ──────────────────────────────────────');
|
|
2558
|
+
const shareHint = isZh
|
|
2559
|
+
? ` 📤 "${wrapped.archetype.name} — 本地扫描到 ${wrapped.total} 个技能,其中 ${wrapped.untappedCount} 个元数据较少。"`
|
|
2560
|
+
: ` 📤 "${wrapped.archetype.name} — ${wrapped.total} local skills scanned, including ${wrapped.untappedCount} with sparse metadata."`;
|
|
2561
|
+
lines.push(shareHint);
|
|
2562
|
+
lines.push(isZh
|
|
2563
|
+
? ' 🔗 使用 --open 生成可分享的 HTML 报告'
|
|
2564
|
+
: ' 🔗 Use --open to generate a shareable HTML report');
|
|
2565
|
+
lines.push('');
|
|
2566
|
+
|
|
2567
|
+
return lines.join('\n');
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function renderWrappedHTML(data) {
|
|
2571
|
+
const skills = data.skills || [];
|
|
2572
|
+
const health = computeHealthStats(skills);
|
|
2573
|
+
const personality = analyzeSkillPersonality(skills);
|
|
2574
|
+
const radar = computeRadarScores(skills, health);
|
|
2575
|
+
const wrapped = computeWrappedStats(skills, health);
|
|
2576
|
+
const isZh = lang() === 'zh';
|
|
2577
|
+
|
|
2578
|
+
function renderRadarChart(dimensions) {
|
|
2579
|
+
const size = 200;
|
|
2580
|
+
const center = size / 2;
|
|
2581
|
+
const radius = 80;
|
|
2582
|
+
const levels = 5;
|
|
2583
|
+
|
|
2584
|
+
function pentagonPoints(r) {
|
|
2585
|
+
return Array.from({ length: 5 }, (_, i) => {
|
|
2586
|
+
const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
|
|
2587
|
+
return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
|
|
2588
|
+
}).join(' ');
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const dataPoints = dimensions.map((d, i) => {
|
|
2592
|
+
const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
|
|
2593
|
+
const r = (d.score / 100) * radius;
|
|
2594
|
+
return `${center + r * Math.cos(angle)},${center + r * Math.sin(angle)}`;
|
|
2595
|
+
}).join(' ');
|
|
2596
|
+
|
|
2597
|
+
const labels = dimensions.map((d, i) => {
|
|
2598
|
+
const angle = (Math.PI * 2 * i) / 5 - Math.PI / 2;
|
|
2599
|
+
const labelR = radius + 25;
|
|
2600
|
+
const x = center + labelR * Math.cos(angle);
|
|
2601
|
+
const y = center + labelR * Math.sin(angle);
|
|
2602
|
+
const anchor = i === 0 ? 'middle' : i < 3 ? 'start' : 'end';
|
|
2603
|
+
return `<text x="${x}" y="${y}" text-anchor="${anchor}" fill="#94a3b8" font-size="11">${d.name}</text>`;
|
|
2604
|
+
}).join('');
|
|
2605
|
+
|
|
2606
|
+
return `
|
|
2607
|
+
<svg viewBox="0 0 ${size} ${size}" class="radar-chart">
|
|
2608
|
+
${Array.from({ length: levels }, (_, i) => {
|
|
2609
|
+
const r = (radius / levels) * (i + 1);
|
|
2610
|
+
return `<polygon points="${pentagonPoints(r)}" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>`;
|
|
2611
|
+
}).join('')}
|
|
2612
|
+
<polygon points="${dataPoints}" fill="rgba(124,58,237,0.3)" stroke="#7c3aed" stroke-width="2"/>
|
|
2613
|
+
${labels}
|
|
2614
|
+
</svg>
|
|
2615
|
+
`;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
const categoryBars = wrapped.categoryBreakdown.slice(0, 6).map(cat => {
|
|
2619
|
+
const barWidth = Math.max(2, cat.percent);
|
|
2620
|
+
const colors = ['#7c3aed', '#06b6d4', '#f59e0b', '#10b981', '#ef4444', '#ec4899'];
|
|
2621
|
+
const color = colors[wrapped.categoryBreakdown.indexOf(cat) % colors.length];
|
|
2622
|
+
return `
|
|
2623
|
+
<div class="dna-row">
|
|
2624
|
+
<span class="dna-name">${escapeHtml(cat.name)}</span>
|
|
2625
|
+
<div class="dna-bar"><div class="dna-fill" style="width:${barWidth}%;background:${color}"></div></div>
|
|
2626
|
+
<span class="dna-count">${cat.count} (${cat.percent}%)</span>
|
|
2627
|
+
</div>`;
|
|
2628
|
+
}).join('');
|
|
2629
|
+
|
|
2630
|
+
const shareText = isZh
|
|
2631
|
+
? `${wrapped.archetype.emoji} ${wrapped.archetype.name} — 本地扫描到 ${wrapped.total} 个技能,其中 ${wrapped.untappedCount} 个元数据较少。`
|
|
2632
|
+
: `${wrapped.archetype.emoji} ${wrapped.archetype.name} — ${wrapped.total} local skills scanned, including ${wrapped.untappedCount} with sparse metadata.`;
|
|
2633
|
+
|
|
2634
|
+
return `<!DOCTYPE html>
|
|
2635
|
+
<html lang="${lang()}">
|
|
2636
|
+
<head>
|
|
2637
|
+
<meta charset="utf-8">
|
|
2638
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2639
|
+
<title>${isZh ? '我的 AI 技能报告' : 'My AI Skill Report'} — skill-guide</title>
|
|
2640
|
+
<meta property="og:title" content="${isZh ? '我的 AI 技能报告' : 'My AI Skill Report'} — skill-guide">
|
|
2641
|
+
<meta property="og:description" content="${escapeHtml(shareText)}">
|
|
2642
|
+
<style>
|
|
2643
|
+
:root {
|
|
2644
|
+
--bg: #0f0f23;
|
|
2645
|
+
--card: #1a1a2e;
|
|
2646
|
+
--text: #e0e0e0;
|
|
2647
|
+
--muted: #888;
|
|
2648
|
+
--accent: #7c3aed;
|
|
2649
|
+
--accent2: #06b6d4;
|
|
2650
|
+
--good: #10b981;
|
|
2651
|
+
}
|
|
2652
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2653
|
+
body {
|
|
2654
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
2655
|
+
background: var(--bg);
|
|
2656
|
+
color: var(--text);
|
|
2657
|
+
min-height: 100vh;
|
|
2658
|
+
}
|
|
2659
|
+
.hero {
|
|
2660
|
+
text-align: center;
|
|
2661
|
+
padding: 4rem 2rem;
|
|
2662
|
+
background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 50%, #0c1222 100%);
|
|
2663
|
+
}
|
|
2664
|
+
.hero-emoji { font-size: 4rem; margin-bottom: 1rem; }
|
|
2665
|
+
.hero-title {
|
|
2666
|
+
font-size: 2.5rem;
|
|
2667
|
+
font-weight: 800;
|
|
2668
|
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
2669
|
+
-webkit-background-clip: text;
|
|
2670
|
+
-webkit-text-fill-color: transparent;
|
|
2671
|
+
margin-bottom: 0.5rem;
|
|
2672
|
+
}
|
|
2673
|
+
.hero-subtitle { color: var(--muted); font-size: 1.1rem; max-width: 600px; margin: 0 auto; }
|
|
2674
|
+
.container { max-width: 900px; margin: 0 auto; padding: 2rem; }
|
|
2675
|
+
.section {
|
|
2676
|
+
background: var(--card);
|
|
2677
|
+
border-radius: 16px;
|
|
2678
|
+
padding: 2rem;
|
|
2679
|
+
margin-bottom: 2rem;
|
|
2680
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
2681
|
+
}
|
|
2682
|
+
.section-title {
|
|
2683
|
+
font-size: 1.25rem;
|
|
2684
|
+
font-weight: 600;
|
|
2685
|
+
margin-bottom: 1.5rem;
|
|
2686
|
+
display: flex;
|
|
2687
|
+
align-items: center;
|
|
2688
|
+
gap: 0.5rem;
|
|
2689
|
+
}
|
|
2690
|
+
.stats-grid {
|
|
2691
|
+
display: grid;
|
|
2692
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
2693
|
+
gap: 1rem;
|
|
2694
|
+
margin-bottom: 1.5rem;
|
|
2695
|
+
}
|
|
2696
|
+
.stat-card {
|
|
2697
|
+
background: rgba(255,255,255,0.05);
|
|
2698
|
+
border-radius: 12px;
|
|
2699
|
+
padding: 1.25rem;
|
|
2700
|
+
text-align: center;
|
|
2701
|
+
}
|
|
2702
|
+
.stat-value {
|
|
2703
|
+
font-size: 2rem;
|
|
2704
|
+
font-weight: 700;
|
|
2705
|
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
2706
|
+
-webkit-background-clip: text;
|
|
2707
|
+
-webkit-text-fill-color: transparent;
|
|
2708
|
+
}
|
|
2709
|
+
.stat-label { color: var(--muted); font-size: 0.85rem; margin-top: 0.25rem; }
|
|
2710
|
+
.compare-row {
|
|
2711
|
+
display: flex;
|
|
2712
|
+
align-items: center;
|
|
2713
|
+
gap: 1rem;
|
|
2714
|
+
padding: 0.75rem 0;
|
|
2715
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
2716
|
+
}
|
|
2717
|
+
.compare-label { width: 140px; font-weight: 500; }
|
|
2718
|
+
.compare-bar-wrap { flex: 1; }
|
|
2719
|
+
.compare-bar {
|
|
2720
|
+
height: 10px;
|
|
2721
|
+
background: rgba(255,255,255,0.1);
|
|
2722
|
+
border-radius: 5px;
|
|
2723
|
+
overflow: hidden;
|
|
2724
|
+
position: relative;
|
|
2725
|
+
}
|
|
2726
|
+
.compare-fill {
|
|
2727
|
+
height: 100%;
|
|
2728
|
+
border-radius: 5px;
|
|
2729
|
+
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
|
2730
|
+
}
|
|
2731
|
+
.compare-value { width: 80px; text-align: right; font-weight: 600; color: var(--accent); }
|
|
2732
|
+
.dna-row {
|
|
2733
|
+
display: flex;
|
|
2734
|
+
align-items: center;
|
|
2735
|
+
gap: 0.75rem;
|
|
2736
|
+
padding: 0.5rem 0;
|
|
2737
|
+
}
|
|
2738
|
+
.dna-name { width: 120px; font-size: 0.9rem; color: var(--muted); }
|
|
2739
|
+
.dna-bar { flex: 1; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }
|
|
2740
|
+
.dna-fill { height: 100%; border-radius: 4px; transition: width 0.5s; }
|
|
2741
|
+
.dna-count { width: 80px; text-align: right; font-size: 0.85rem; color: var(--muted); }
|
|
2742
|
+
.radar-container { display: flex; justify-content: center; padding: 1rem; }
|
|
2743
|
+
.radar-chart { width: min(280px, 80vw); height: min(280px, 80vw); }
|
|
2744
|
+
.share-section {
|
|
2745
|
+
text-align: center;
|
|
2746
|
+
padding: 2rem;
|
|
2747
|
+
background: linear-gradient(135deg, rgba(124,58,237,0.1), rgba(6,182,212,0.1));
|
|
2748
|
+
border-radius: 16px;
|
|
2749
|
+
margin-top: 2rem;
|
|
2750
|
+
}
|
|
2751
|
+
.share-btn {
|
|
2752
|
+
display: inline-block;
|
|
2753
|
+
padding: 0.75rem 2rem;
|
|
2754
|
+
border-radius: 8px;
|
|
2755
|
+
font-weight: 600;
|
|
2756
|
+
text-decoration: none;
|
|
2757
|
+
font-size: 1rem;
|
|
2758
|
+
cursor: pointer;
|
|
2759
|
+
border: none;
|
|
2760
|
+
margin: 0.5rem;
|
|
2761
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
2762
|
+
}
|
|
2763
|
+
.share-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(124,58,237,0.3); }
|
|
2764
|
+
.share-btn.primary { background: linear-gradient(135deg, #7c3aed, #06b6d4); color: #fff; }
|
|
2765
|
+
.share-btn.secondary { background: rgba(255,255,255,0.1); color: var(--text); }
|
|
2766
|
+
.share-text {
|
|
2767
|
+
background: var(--card);
|
|
2768
|
+
border-radius: 12px;
|
|
2769
|
+
padding: 1.5rem;
|
|
2770
|
+
margin: 1rem auto;
|
|
2771
|
+
max-width: 600px;
|
|
2772
|
+
text-align: left;
|
|
2773
|
+
font-size: 0.95rem;
|
|
2774
|
+
line-height: 1.6;
|
|
2775
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
2776
|
+
}
|
|
2777
|
+
.cta {
|
|
2778
|
+
text-align: center;
|
|
2779
|
+
padding: 2rem;
|
|
2780
|
+
margin-top: 2rem;
|
|
2781
|
+
}
|
|
2782
|
+
.cta code {
|
|
2783
|
+
background: var(--card);
|
|
2784
|
+
padding: 0.5rem 1rem;
|
|
2785
|
+
border-radius: 8px;
|
|
2786
|
+
font-size: 1.1rem;
|
|
2787
|
+
display: inline-block;
|
|
2788
|
+
margin: 0.5rem 0;
|
|
2789
|
+
}
|
|
2790
|
+
.footer {
|
|
2791
|
+
text-align: center;
|
|
2792
|
+
padding: 2rem;
|
|
2793
|
+
color: var(--muted);
|
|
2794
|
+
font-size: 0.85rem;
|
|
2795
|
+
}
|
|
2796
|
+
.gap-visual {
|
|
2797
|
+
display: flex;
|
|
2798
|
+
align-items: center;
|
|
2799
|
+
justify-content: center;
|
|
2800
|
+
gap: 2rem;
|
|
2801
|
+
margin: 1.5rem 0;
|
|
2802
|
+
}
|
|
2803
|
+
.gap-core, .gap-ready, .gap-untapped {
|
|
2804
|
+
text-align: center;
|
|
2805
|
+
flex: 1;
|
|
2806
|
+
padding: 1.25rem;
|
|
2807
|
+
border-radius: 12px;
|
|
2808
|
+
}
|
|
2809
|
+
.gap-core {
|
|
2810
|
+
background: rgba(124,58,237,0.1);
|
|
2811
|
+
border: 1px solid rgba(124,58,237,0.2);
|
|
2812
|
+
}
|
|
2813
|
+
.gap-ready {
|
|
2814
|
+
background: rgba(6,182,212,0.08);
|
|
2815
|
+
border: 1px solid rgba(6,182,212,0.15);
|
|
2816
|
+
}
|
|
2817
|
+
.gap-untapped {
|
|
2818
|
+
background: rgba(245,158,11,0.08);
|
|
2819
|
+
border: 1px solid rgba(245,158,11,0.15);
|
|
2820
|
+
}
|
|
2821
|
+
.gap-number {
|
|
2822
|
+
font-size: 2.5rem;
|
|
2823
|
+
font-weight: 800;
|
|
2824
|
+
}
|
|
2825
|
+
.gap-core .gap-number { color: var(--accent); }
|
|
2826
|
+
.gap-ready .gap-number { color: var(--accent2); }
|
|
2827
|
+
.gap-untapped .gap-number { color: #f59e0b; }
|
|
2828
|
+
.gap-label { font-weight: 600; margin-top: 0.25rem; }
|
|
2829
|
+
.gap-detail { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
|
|
2830
|
+
.gap-divider { display: flex; align-items: center; }
|
|
2831
|
+
.gap-vs {
|
|
2832
|
+
background: rgba(255,255,255,0.1);
|
|
2833
|
+
color: var(--muted);
|
|
2834
|
+
font-weight: 700;
|
|
2835
|
+
font-size: 0.85rem;
|
|
2836
|
+
width: 36px;
|
|
2837
|
+
height: 36px;
|
|
2838
|
+
border-radius: 50%;
|
|
2839
|
+
display: flex;
|
|
2840
|
+
align-items: center;
|
|
2841
|
+
justify-content: center;
|
|
2842
|
+
}
|
|
2843
|
+
.gap-bar-wrap {
|
|
2844
|
+
display: flex;
|
|
2845
|
+
height: 8px;
|
|
2846
|
+
border-radius: 4px;
|
|
2847
|
+
overflow: hidden;
|
|
2848
|
+
margin-top: 1rem;
|
|
2849
|
+
}
|
|
2850
|
+
.gap-bar-core { background: var(--accent); }
|
|
2851
|
+
.gap-bar-ready { background: var(--accent2); }
|
|
2852
|
+
.gap-bar-untapped { background: #f59e0b; }
|
|
2853
|
+
.gap-bar-labels {
|
|
2854
|
+
display: flex;
|
|
2855
|
+
justify-content: space-between;
|
|
2856
|
+
font-size: 0.8rem;
|
|
2857
|
+
color: var(--muted);
|
|
2858
|
+
margin-top: 0.4rem;
|
|
2859
|
+
}
|
|
2860
|
+
@media (max-width: 600px) {
|
|
2861
|
+
.hero-title { font-size: 1.8rem; }
|
|
2862
|
+
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
2863
|
+
.gap-visual { flex-direction: column; gap: 1rem; }
|
|
2864
|
+
.gap-divider { transform: rotate(90deg); }
|
|
2865
|
+
}
|
|
2866
|
+
</style>
|
|
2867
|
+
</head>
|
|
2868
|
+
<body>
|
|
2869
|
+
<div class="hero">
|
|
2870
|
+
<div class="hero-emoji">${wrapped.archetype.emoji}</div>
|
|
2871
|
+
<h1 class="hero-title">${isZh ? wrapped.archetype.name : wrapped.archetype.name}</h1>
|
|
2872
|
+
<p class="hero-subtitle">${escapeHtml(wrapped.archetype.tagline)}</p>
|
|
2873
|
+
</div>
|
|
2874
|
+
|
|
2875
|
+
<div class="container">
|
|
2876
|
+
<div class="section">
|
|
2877
|
+
<h2 class="section-title">${isZh ? '📊 你的数据' : '📊 Your Stats'}</h2>
|
|
2878
|
+
<div class="stats-grid">
|
|
2879
|
+
<div class="stat-card">
|
|
2880
|
+
<div class="stat-value">${wrapped.total}</div>
|
|
2881
|
+
<div class="stat-label">${isZh ? '总技能数' : 'Total Skills'}</div>
|
|
2882
|
+
</div>
|
|
2883
|
+
<div class="stat-card">
|
|
2884
|
+
<div class="stat-value">${wrapped.categoryCount}</div>
|
|
2885
|
+
<div class="stat-label">${isZh ? '覆盖领域' : 'Categories'}</div>
|
|
2886
|
+
</div>
|
|
2887
|
+
<div class="stat-card">
|
|
2888
|
+
<div class="stat-value">${(wrapped.totalTokens / 1000).toFixed(1)}K</div>
|
|
2889
|
+
<div class="stat-label">${isZh ? '描述 Token 估算' : 'Description Token Estimate'}</div>
|
|
2890
|
+
</div>
|
|
2891
|
+
</div>
|
|
2892
|
+
</div>
|
|
2893
|
+
|
|
2894
|
+
<div class="section" style="background:linear-gradient(135deg, rgba(245,158,11,0.08), rgba(124,58,237,0.08));border:1px solid rgba(245,158,11,0.15)">
|
|
2895
|
+
<h2 class="section-title">${isZh ? '⚡ 元数据完整度' : '⚡ Metadata Completeness'}</h2>
|
|
2896
|
+
<p style="color:var(--muted);margin-bottom:1rem;font-size:0.9rem">
|
|
2897
|
+
${isZh ? '基于描述完整度、工具配置和触发词' : 'Based on description depth, tool config, and triggers'}
|
|
2898
|
+
</p>
|
|
2899
|
+
<div class="gap-visual">
|
|
2900
|
+
<div class="gap-core">
|
|
2901
|
+
<div class="gap-number">${wrapped.coreCount}</div>
|
|
2902
|
+
<div class="gap-label">${isZh ? '元数据较完整' : 'Richer Metadata'}</div>
|
|
2903
|
+
<div class="gap-detail">${isZh ? '描述、工具或触发词较完整' : 'More description, tool, or trigger metadata'}</div>
|
|
2904
|
+
</div>
|
|
2905
|
+
<div class="gap-divider"><div class="gap-vs">+</div></div>
|
|
2906
|
+
<div class="gap-ready">
|
|
2907
|
+
<div class="gap-number">${wrapped.readyCount}</div>
|
|
2908
|
+
<div class="gap-label">${isZh ? '元数据一般' : 'Partial Metadata'}</div>
|
|
2909
|
+
<div class="gap-detail">${isZh ? '可以按需补充' : 'Review whether more detail is useful'}</div>
|
|
2910
|
+
</div>
|
|
2911
|
+
<div class="gap-divider"><div class="gap-vs">+</div></div>
|
|
2912
|
+
<div class="gap-untapped">
|
|
2913
|
+
<div class="gap-number">${wrapped.untappedCount}</div>
|
|
2914
|
+
<div class="gap-label">${isZh ? '元数据较少' : 'Sparse Metadata'}</div>
|
|
2915
|
+
<div class="gap-detail">${isZh ? '建议复核描述和触发词' : 'Review descriptions and triggers'}</div>
|
|
2916
|
+
</div>
|
|
2917
|
+
</div>
|
|
2918
|
+
<div class="gap-bar-wrap">
|
|
2919
|
+
<div class="gap-bar-core" style="width:${wrapped.corePercent}%"></div>
|
|
2920
|
+
<div class="gap-bar-ready" style="width:${wrapped.readyPercent}%"></div>
|
|
2921
|
+
<div class="gap-bar-untapped" style="width:${wrapped.untappedPercent}%"></div>
|
|
2922
|
+
</div>
|
|
2923
|
+
<div class="gap-bar-labels">
|
|
2924
|
+
<span>${wrapped.corePercent}% ${isZh ? '较完整' : 'richer'}</span>
|
|
2925
|
+
<span>${wrapped.readyPercent}% ${isZh ? '一般' : 'partial'}</span>
|
|
2926
|
+
<span>${wrapped.untappedPercent}% ${isZh ? '较少' : 'sparse'}</span>
|
|
2927
|
+
</div>
|
|
2928
|
+
</div>
|
|
2929
|
+
|
|
2930
|
+
<div class="section">
|
|
2931
|
+
<h2 class="section-title">${isZh ? '📍 本地画像说明' : '📍 Local Profile Notes'}</h2>
|
|
2932
|
+
<p style="color:var(--muted);font-size:0.9rem">
|
|
2933
|
+
${isZh
|
|
2934
|
+
? '此报告只使用当前设备扫描到的技能元数据。它不会推断社区排名、实际使用频率或删除安全性。'
|
|
2935
|
+
: 'This report uses skill metadata scanned on this device only. It does not infer community rank, actual usage, or whether deletion is safe.'}
|
|
2936
|
+
</p>
|
|
2937
|
+
</div>
|
|
2938
|
+
|
|
2939
|
+
<div class="section">
|
|
2940
|
+
<h2 class="section-title">${isZh ? '🧬 技能 DNA' : '🧬 Skill DNA'}</h2>
|
|
2941
|
+
${categoryBars}
|
|
2942
|
+
</div>
|
|
2943
|
+
|
|
2944
|
+
<div class="section">
|
|
2945
|
+
<h2 class="section-title">${isZh ? '📈 五维健康雷达' : '📈 Health Radar'}</h2>
|
|
2946
|
+
<div class="radar-container">
|
|
2947
|
+
${renderRadarChart(radar.dimensions)}
|
|
2948
|
+
</div>
|
|
2949
|
+
</div>
|
|
2950
|
+
|
|
2951
|
+
<div class="share-section">
|
|
2952
|
+
<h2>${isZh ? '📤 分享你的报告' : '📤 Share Your Report'}</h2>
|
|
2953
|
+
<p style="color:var(--muted);margin:0.5rem 0">${isZh ? '让朋友也看看自己的技能 DNA' : 'Let friends discover their skill DNA'}</p>
|
|
2954
|
+
<div class="share-text">${escapeHtml(shareText)}</div>
|
|
2955
|
+
<button class="share-btn primary" onclick="copyShare()">${isZh ? '复制分享文案' : 'Copy Share Text'}</button>
|
|
2956
|
+
<button class="share-btn secondary" onclick="copyLink()">${isZh ? '复制命令' : 'Copy Command'}</button>
|
|
2957
|
+
</div>
|
|
2958
|
+
|
|
2959
|
+
<div class="cta">
|
|
2960
|
+
<p style="color:var(--muted);margin-bottom:0.5rem">${isZh ? '生成你自己的技能报告' : 'Generate your own skill report'}</p>
|
|
2961
|
+
<code>npx skill-guide --wrapped --open</code>
|
|
2962
|
+
</div>
|
|
2963
|
+
</div>
|
|
2964
|
+
|
|
2965
|
+
<div class="footer">
|
|
2966
|
+
<p>Powered by <a href="https://github.com/gtskevin/skill-guide" style="color:var(--accent)">skill-guide</a></p>
|
|
2967
|
+
</div>
|
|
2968
|
+
|
|
2969
|
+
<script>
|
|
2970
|
+
function copyShare() {
|
|
2971
|
+
navigator.clipboard.writeText(${JSON.stringify(shareText + '\n\nnpx skill-guide --wrapped --open')}).then(() => {
|
|
2972
|
+
alert('${isZh ? '已复制到剪贴板!' : 'Copied to clipboard!'}');
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
function copyLink() {
|
|
2976
|
+
navigator.clipboard.writeText('npx skill-guide --wrapped --open').then(() => {
|
|
2977
|
+
alert('${isZh ? '已复制到剪贴板!' : 'Copied to clipboard!'}');
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
</script>
|
|
2981
|
+
</body>
|
|
2982
|
+
</html>`;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
function generatePrescription(skills, health) {
|
|
2986
|
+
const prescriptions = [];
|
|
2987
|
+
const totalTokens = skills.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
|
|
2988
|
+
|
|
2989
|
+
const topConsumers = [...skills]
|
|
2990
|
+
.sort((a, b) => (b.tokenCost || 0) - (a.tokenCost || 0))
|
|
2991
|
+
.slice(0, 5);
|
|
2992
|
+
|
|
2993
|
+
if (topConsumers.length > 0) {
|
|
2994
|
+
const topTokens = topConsumers.reduce((sum, s) => sum + (s.tokenCost || 0), 0);
|
|
2995
|
+
const percent = totalTokens > 0 ? (topTokens / totalTokens * 100).toFixed(0) : 0;
|
|
2996
|
+
const isSignificant = parseInt(percent) >= 10;
|
|
2997
|
+
|
|
2998
|
+
prescriptions.push({
|
|
2999
|
+
type: 'optimize',
|
|
3000
|
+
emoji: isSignificant ? '🎯' : '💡',
|
|
3001
|
+
title: isSignificant ? '描述复核' : '优化建议',
|
|
3002
|
+
titleEn: isSignificant ? 'Description Review' : 'Optimization Tips',
|
|
3003
|
+
description: isSignificant
|
|
3004
|
+
? `这 ${topConsumers.length} 个技能占描述 Token 估算的 ${percent}%,建议复核是否需要精简`
|
|
3005
|
+
: `你的技能库很均衡,没有明显的瘦身空间。Top ${topConsumers.length} 只占 ${percent}%`,
|
|
3006
|
+
descriptionEn: isSignificant
|
|
3007
|
+
? `These ${topConsumers.length} skills account for ${percent}% of estimated description tokens; review whether their descriptions should be shortened`
|
|
3008
|
+
: `Your skill library is well-balanced. Top ${topConsumers.length} only account for ${percent}%`,
|
|
3009
|
+
items: topConsumers.map(s => ({
|
|
3010
|
+
name: s.name,
|
|
3011
|
+
tokens: s.tokenCost,
|
|
3012
|
+
action: isSignificant ? '复核并按需精简描述' : '保持现状',
|
|
3013
|
+
actionEn: isSignificant ? 'Review and shorten the description if appropriate' : 'Keep as is',
|
|
3014
|
+
})),
|
|
3015
|
+
impact: isSignificant ? 'high' : 'low',
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
if (health.securityFlags.length > 0) {
|
|
3020
|
+
prescriptions.push({
|
|
3021
|
+
type: 'security',
|
|
3022
|
+
emoji: '🛡️',
|
|
3023
|
+
title: '安全审查',
|
|
3024
|
+
titleEn: 'Security Review',
|
|
3025
|
+
description: `${health.securityFlags.length} 个技能有安全风险标记,建议人工审查`,
|
|
3026
|
+
descriptionEn: `${health.securityFlags.length} skills have security flags, recommend manual review`,
|
|
3027
|
+
items: health.securityFlags.slice(0, 5).map(s => ({
|
|
3028
|
+
name: s.name,
|
|
3029
|
+
flags: s.flags,
|
|
3030
|
+
action: '审查权限和命令',
|
|
3031
|
+
actionEn: 'Review permissions and commands',
|
|
3032
|
+
})),
|
|
3033
|
+
impact: 'medium',
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
if (health.duplicateGroups.length > 0) {
|
|
3038
|
+
prescriptions.push({
|
|
3039
|
+
type: 'dedup',
|
|
3040
|
+
emoji: '🔄',
|
|
3041
|
+
title: '去重优化',
|
|
3042
|
+
titleEn: 'Deduplication',
|
|
3043
|
+
description: `发现 ${health.duplicateGroups.length} 组同名技能,请复核来源和自定义改动`,
|
|
3044
|
+
descriptionEn: `Found ${health.duplicateGroups.length} same-name groups; review sources and custom changes`,
|
|
3045
|
+
items: health.duplicateGroups.slice(0, 3).map(g => ({
|
|
3046
|
+
name: g.names.join(' = '),
|
|
3047
|
+
action: '复核来源后再决定是否处理',
|
|
3048
|
+
actionEn: 'Review sources before deciding what to change',
|
|
3049
|
+
})),
|
|
3050
|
+
impact: 'low',
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
if (health.budgetUsedPercent > 100) {
|
|
3055
|
+
const overAmount = health.totalDescriptionLength - health.descriptionBudget;
|
|
3056
|
+
prescriptions.push({
|
|
3057
|
+
type: 'budget',
|
|
3058
|
+
emoji: '📦',
|
|
3059
|
+
title: '预算超支',
|
|
3060
|
+
titleEn: 'Budget Overage',
|
|
3061
|
+
description: `描述总长度超出本地参考预算 ${overAmount.toLocaleString()} 字符,建议复核较长描述`,
|
|
3062
|
+
descriptionEn: `Total description length exceeds the local reference budget by ${overAmount.toLocaleString()} chars; review longer descriptions`,
|
|
3063
|
+
items: [{
|
|
3064
|
+
name: '整体',
|
|
3065
|
+
action: '精简技能描述,删除不必要的细节',
|
|
3066
|
+
actionEn: 'Shorten skill descriptions, remove unnecessary details',
|
|
3067
|
+
}],
|
|
3068
|
+
impact: 'high',
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
return prescriptions;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
741
3075
|
function main() {
|
|
742
3076
|
const mode = parseMode();
|
|
743
3077
|
if (mode.mode === 'help') {
|
|
@@ -745,16 +3079,108 @@ function main() {
|
|
|
745
3079
|
return;
|
|
746
3080
|
}
|
|
747
3081
|
|
|
748
|
-
|
|
3082
|
+
// --doctor: terminal-only diagnostic
|
|
749
3083
|
if (mode.mode === 'doctor') {
|
|
3084
|
+
const data = runScanner(mode);
|
|
750
3085
|
process.stdout.write(`${printDoctor(data)}\n`);
|
|
751
3086
|
return;
|
|
752
3087
|
}
|
|
753
3088
|
|
|
754
3089
|
const format = getArgValue('--format') || 'html';
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
3090
|
+
|
|
3091
|
+
// --find mode: try skill deep-dive, fall back to search
|
|
3092
|
+
if (mode.mode === 'find') {
|
|
3093
|
+
const skillData = runScanner(mode);
|
|
3094
|
+
if (skillData.error) {
|
|
3095
|
+
// Skill not found — fall back to search
|
|
3096
|
+
mode.mode = 'search';
|
|
3097
|
+
const searchArgs = ['--search', mode.value];
|
|
3098
|
+
if (hasFlag('--refresh')) searchArgs.push('--refresh');
|
|
3099
|
+
const result = spawnSync(process.execPath, [SCANNER, ...searchArgs], {
|
|
3100
|
+
cwd: process.cwd(), encoding: 'utf8',
|
|
3101
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3102
|
+
});
|
|
3103
|
+
if (result.status !== 0) {
|
|
3104
|
+
process.stderr.write(result.stderr || '');
|
|
3105
|
+
process.exit(result.status || 1);
|
|
3106
|
+
}
|
|
3107
|
+
const data = JSON.parse(result.stdout);
|
|
3108
|
+
if (format === 'json') {
|
|
3109
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
process.stdout.write(renderDefaultTerminal(data.skills || []));
|
|
3113
|
+
const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
|
|
3114
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
3115
|
+
fs.writeFileSync(output, renderHtml(data, mode), 'utf8');
|
|
3116
|
+
if (shouldAutoOpen()) openFile(output);
|
|
3117
|
+
process.stdout.write(`Generated ${output}\n`);
|
|
3118
|
+
} else {
|
|
3119
|
+
// Skill found — deep dive
|
|
3120
|
+
mode.mode = 'skill';
|
|
3121
|
+
if (format === 'json') {
|
|
3122
|
+
process.stdout.write(JSON.stringify(skillData, null, 2) + '\n');
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
|
|
3126
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
3127
|
+
fs.writeFileSync(output, renderHtml(skillData, mode), 'utf8');
|
|
3128
|
+
if (shouldAutoOpen()) openFile(output);
|
|
3129
|
+
process.stdout.write(`Generated ${output}\n`);
|
|
3130
|
+
}
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// --recommend mode (online registry data)
|
|
3135
|
+
if (mode.mode === 'recommend') {
|
|
3136
|
+
const data = runScanner(mode);
|
|
3137
|
+
const installed = data.skills;
|
|
3138
|
+
const refresh = hasFlag('--refresh');
|
|
3139
|
+
const onlineEntries = registryModule.fetchRegistry({ refresh });
|
|
3140
|
+
const recommendations = registryModule.recommend(installed, onlineEntries);
|
|
3141
|
+
if (format === 'json') {
|
|
3142
|
+
process.stdout.write(JSON.stringify({ installed, recommendations }, null, 2) + '\n');
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
process.stdout.write(renderRecommendTerminal(data, recommendations));
|
|
3146
|
+
const outputFile = getArgValue('--output');
|
|
3147
|
+
const html = renderRecommendHTML(data, recommendations, getArgValue('--user'));
|
|
3148
|
+
const defaultFile = path.join(os.tmpdir(), 'skill-guide-recommend.html');
|
|
3149
|
+
const targetFile = outputFile ? path.resolve(outputFile) : defaultFile;
|
|
3150
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
3151
|
+
fs.writeFileSync(targetFile, html, 'utf8');
|
|
3152
|
+
if (shouldAutoOpen()) openFile(targetFile);
|
|
3153
|
+
process.stdout.write(`Generated: ${targetFile}\n`);
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// --share mode (portfolio with --user flag)
|
|
3158
|
+
if (mode.mode === 'share') {
|
|
3159
|
+
const data = runScanner(mode);
|
|
3160
|
+
const user = getArgValue('--user');
|
|
3161
|
+
if (format === 'json') {
|
|
3162
|
+
process.stdout.write(JSON.stringify({ skills: data.skills, totalCount: data.totalCount }, null, 2) + '\n');
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const outputFile = getArgValue('--output');
|
|
3166
|
+
const html = renderShareHTML(data, user);
|
|
3167
|
+
const defaultFile = path.join(os.tmpdir(), 'skill-guide-share.html');
|
|
3168
|
+
const targetFile = outputFile ? path.resolve(outputFile) : defaultFile;
|
|
3169
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
3170
|
+
fs.writeFileSync(targetFile, html, 'utf8');
|
|
3171
|
+
if (shouldAutoOpen()) openFile(targetFile);
|
|
3172
|
+
process.stdout.write(`Generated: ${targetFile}\n`);
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// Default mode (list): overview dashboard
|
|
3177
|
+
const data = runScanner(mode);
|
|
3178
|
+
|
|
3179
|
+
// Filter by platform unless --all
|
|
3180
|
+
const platform = hasFlag('--all') ? 'all' : detectPlatform();
|
|
3181
|
+
if (platform !== 'all' && data.skills) {
|
|
3182
|
+
data.skills = filterSkillsByPlatform(data.skills, platform);
|
|
3183
|
+
data.totalCount = data.skills.length;
|
|
758
3184
|
}
|
|
759
3185
|
|
|
760
3186
|
if (format === 'json') {
|
|
@@ -771,11 +3197,23 @@ function main() {
|
|
|
771
3197
|
return;
|
|
772
3198
|
}
|
|
773
3199
|
|
|
3200
|
+
if (data.error) {
|
|
3201
|
+
process.stderr.write(`${formatScannerError(data)}\n`);
|
|
3202
|
+
process.exit(1);
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
// Terminal output
|
|
3206
|
+
if (platform !== 'all') {
|
|
3207
|
+
const platformLabel = platform === 'codex' ? 'Codex' : 'Claude Code';
|
|
3208
|
+
process.stdout.write(` Showing skills for: ${platformLabel} (use --all to see all)\n\n`);
|
|
3209
|
+
}
|
|
3210
|
+
process.stdout.write(renderDefaultTerminal(data.skills || []));
|
|
3211
|
+
|
|
774
3212
|
const output = path.resolve(getArgValue('--output') || defaultOutputPath(mode));
|
|
775
3213
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
776
3214
|
fs.writeFileSync(output, renderHtml(data, mode), 'utf8');
|
|
777
3215
|
|
|
778
|
-
if (
|
|
3216
|
+
if (shouldAutoOpen()) openFile(output);
|
|
779
3217
|
process.stdout.write(`Generated ${output}\n`);
|
|
780
3218
|
}
|
|
781
3219
|
|