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/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 [--open] [--output <file>] [--format html|json] [--lang en|zh] [--refresh]',
27
- ' skill-guide --search <query> [--open] [--output <file>] [--format html|json] [--lang en|zh]',
28
- ' skill-guide --skill <name> [--open] [--output <file>] [--format html|json] [--lang en|zh]',
29
- ' skill-guide --full [--open] [--output <file>] [--format html|json] [--lang en|zh]',
30
- ' skill-guide --doctor [--refresh]',
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 --open',
34
- ' npx skill-guide --search security --open',
35
- ' npx skill-guide --skill tdd --lang zh --open',
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('--full') || args[0] === 'all') return { mode: 'full' };
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
- const search = getArgValue('--search');
328
- if (search) return { mode: 'search', value: search };
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
- const valueFlags = new Set(['--output', '--skill', '--search', '--format', '--lang']);
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: 'skill', value: positional };
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 === 'skill') {
410
+ } else if (mode.mode === 'find') {
411
+ // Try as skill first, fall back to search
344
412
  scannerArgs.push('--skill', mode.value);
345
- } else if (mode.mode === 'search') {
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
- if (mode.mode === 'full') {
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 highlights = [...skills]
502
- .sort((a, b) => ((b.triggers || []).length + (b.sources || []).length) - ((a.triggers || []).length + (a.sources || []).length))
503
- .slice(0, 8);
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">${t('highlights')}</h2>
508
- <div class="list">${highlights.map((skill, index) => `<article class="row">
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.category)}${sourceBadges(skill.sources)}</div>
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
- const data = runScanner(mode);
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
- if (!['html', 'json'].includes(format)) {
756
- process.stderr.write('Error: --format must be "html" or "json"\n');
757
- process.exit(1);
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 (hasFlag('--open') && !hasFlag('--no-open')) openFile(output);
3216
+ if (shouldAutoOpen()) openFile(output);
779
3217
  process.stdout.write(`Generated ${output}\n`);
780
3218
  }
781
3219